[{"data":1,"prerenderedAt":6285},["ShallowReactive",2],{"navigation":3,"/blog/case-study/ticketing-events-platform-case-study-post":734,"/blog/case-study/ticketing-events-platform-case-study-surround":922,"/blog/case-study/ticketing-events-platform-case-study-related":927},[4,70,207,312,721],{"title":5,"path":6,"stem":7,"children":8,"page":69},"Case Study","/blog/case-study","blog/case-study",[9,13,17,21,25,29,33,37,41,45,49,53,57,61,65],{"title":10,"path":11,"stem":12},"Ambistream – Multi-Layer Streaming Platform","/blog/case-study/ambistream-building-a-multi-layer-streaming-platform-from-a-spark-of-an-idea","blog/case-study/ambistream-building-a-multi-layer-streaming-platform-from-a-spark-of-an-idea",{"title":14,"path":15,"stem":16},"Custom Chromecast & AirPlay Casting App","/blog/case-study/chromecast-airplay-casting-app-case-study","blog/case-study/chromecast-airplay-casting-app-case-study",{"title":18,"path":19,"stem":20},"Direct Music Licensing Platform Case Study","/blog/case-study/dlm-music-catalog-case-study","blog/case-study/dlm-music-catalog-case-study",{"title":22,"path":23,"stem":24},"Assembly Instructions App - Galeco Mobile App","/blog/case-study/galeco-mobile-app-case-study","blog/case-study/galeco-mobile-app-case-study",{"title":26,"path":27,"stem":28},"MemoSonic: Building an Audio Memory Game","/blog/case-study/how-we-built-memosonic-accessible-audio-memory-game-flutter","blog/case-study/how-we-built-memosonic-accessible-audio-memory-game-flutter",{"title":30,"path":31,"stem":32},"Loyalty Program App for Shopping Mall","/blog/case-study/loyalty-program-application-case-study","blog/case-study/loyalty-program-application-case-study",{"title":34,"path":35,"stem":36},"Mobile Application for Music Catalogs","/blog/case-study/mobile-app-for-music-catalog","blog/case-study/mobile-app-for-music-catalog",{"title":38,"path":39,"stem":40},"Universal Music Data Parser for 20+ Platforms","/blog/case-study/musicdata-lab-universal-music-data-parser-case-study","blog/case-study/musicdata-lab-universal-music-data-parser-case-study",{"title":42,"path":43,"stem":44},"Panther ML/AI Pricing Recommendation Tool","/blog/case-study/panther-pricing-recommendation-tool-case-study","blog/case-study/panther-pricing-recommendation-tool-case-study",{"title":46,"path":47,"stem":48},"4D Grupa Roofing Wholesalers Platform","/blog/case-study/roofing-wholesalers-website-case-study","blog/case-study/roofing-wholesalers-website-case-study",{"title":50,"path":51,"stem":52},"Talent Alpha HR Frontend Platform","/blog/case-study/talent-alpha-hr-platform-case-study","blog/case-study/talent-alpha-hr-platform-case-study",{"title":54,"path":55,"stem":56},"Ticketing & Events Platform Development","/blog/case-study/ticketing-events-platform-case-study","blog/case-study/ticketing-events-platform-case-study",{"title":58,"path":59,"stem":60},"Turn Fans into Superfans — Roadie.co","/blog/case-study/turn-fans-into-superfans-roadie-co","blog/case-study/turn-fans-into-superfans-roadie-co",{"title":62,"path":63,"stem":64},"Walkative 2.0 Global Booking Engine","/blog/case-study/walkative-2-booking-platform-case-study","blog/case-study/walkative-2-booking-platform-case-study",{"title":66,"path":67,"stem":68},"Why Merch Is the New Royalty Check, and How Tech Is Closing the Gap","/blog/case-study/why-merch-is-the-new-royalty-check","blog/case-study/why-merch-is-the-new-royalty-check",false,{"title":71,"path":72,"stem":73,"children":74,"page":69},"Music Data","/blog/music-data","blog/music-data",[75,79,83,87,91,95,99,103,107,111,115,119,123,127,131,135,139,143,147,151,155,159,163,167,171,175,179,183,187,191,195,199,203],{"title":76,"path":77,"stem":78},"13 Distributors, 5 File Formats, Zero Standards -The Reality of Music Royalty Data","/blog/music-data/13-distributors-5-file-formats-zero-standards-the-reality-of-music-royalty-data","blog/music-data/13-distributors-5-file-formats-zero-standards-the-reality-of-music-royalty-data",{"title":80,"path":81,"stem":82},"830 Ways to Say Spotify - Normalizing Music Streaming Data","/blog/music-data/830-ways-to-say-spotify-normalizing-music-streaming-data","blog/music-data/830-ways-to-say-spotify-normalizing-music-streaming-data",{"title":84,"path":85,"stem":86},"Why Music Companies Need AI-Powered Analytics (And How We Built One)","/blog/music-data/ai-powered-analytics-dashboard-django-clickhouse-ollama","blog/music-data/ai-powered-analytics-dashboard-django-clickhouse-ollama",{"title":88,"path":89,"stem":90},"AI Rehearsal: Spaced Repetition for Your Musical Ideas","/blog/music-data/ai-rehearsal-spaced-repetition-for-musical-ideas","blog/music-data/ai-rehearsal-spaced-repetition-for-musical-ideas",{"title":92,"path":93,"stem":94},"Audio Project Organization Is a Mess — Here's Why","/blog/music-data/audio-project-organization-mess","blog/music-data/audio-project-organization-mess",{"title":96,"path":97,"stem":98},"Why Audio Search Is Still Broken and How to Fix It with Embeddings","/blog/music-data/audio-search-broken-fix-with-embeddings","blog/music-data/audio-search-broken-fix-with-embeddings",{"title":100,"path":101,"stem":102},"AI Song Structure Analysis: Intro, Verse, Chorus","/blog/music-data/automatic-song-structure-analysis-how-ai-detects-intro-verse-chorus","blog/music-data/automatic-song-structure-analysis-how-ai-detects-intro-verse-chorus",{"title":104,"path":105,"stem":106},"The Broken Feedback Loop in Music Collaboration","/blog/music-data/broken-feedback-loop-music-collaboration","blog/music-data/broken-feedback-loop-music-collaboration",{"title":108,"path":109,"stem":110},"Building a Claude Skill for DDEX Validation: Automate Music Metadata Checks with AI","/blog/music-data/building-a-claude-skill-for-ddex-validation-music-metadata","blog/music-data/building-a-claude-skill-for-ddex-validation-music-metadata",{"title":112,"path":113,"stem":114},"Building a Custom Music Delivery Platform on the Revelator API","/blog/music-data/building-a-custom-music-delivery-platform-on-the-revelator-api","blog/music-data/building-a-custom-music-delivery-platform-on-the-revelator-api",{"title":116,"path":117,"stem":118},"Building a Suno AI Remix App with Nuxt & Firebase","/blog/music-data/building-a-suno-remix-app-with-nuxt-and-firebase","blog/music-data/building-a-suno-remix-app-with-nuxt-and-firebase",{"title":120,"path":121,"stem":122},"C2PA & DDEX: Authenticity Meets Rights in the Age of AI Music","/blog/music-data/c2pa-and-ddex-authenticity-meets-rights-in-the-age-of-ai-music","blog/music-data/c2pa-and-ddex-authenticity-meets-rights-in-the-age-of-ai-music",{"title":124,"path":125,"stem":126},"C2PA in Music: A Claude MCP for Reading Content Provenance","/blog/music-data/c2pa-in-music-mcp","blog/music-data/c2pa-in-music-mcp",{"title":128,"path":129,"stem":130},"Data Modeling in MongoDB Using Design Patterns","/blog/music-data/data-modeling-in-mongodb-with-the-usage-of-design-patterns","blog/music-data/data-modeling-in-mongodb-with-the-usage-of-design-patterns",{"title":132,"path":133,"stem":134},"Office Hours with MusicTech Lab's DDEX Expert","/blog/music-data/ddex-office-hours-musictech","blog/music-data/ddex-office-hours-musictech",{"title":136,"path":137,"stem":138},"DDEX Open Source Projects Review","/blog/music-data/ddex-open-source-projects-review","blog/music-data/ddex-open-source-projects-review",{"title":140,"path":141,"stem":142},"Extracting Data from Ableton .als and .asd Files","/blog/music-data/extracting-data-from-ableton-als-asd-files","blog/music-data/extracting-data-from-ableton-als-asd-files",{"title":144,"path":145,"stem":146},"Maciej Dulski on Sound Connections Podcast","/blog/music-data/from-startups-to-musictech-maciej-dulski-on-sound-connections-podcast","blog/music-data/from-startups-to-musictech-maciej-dulski-on-sound-connections-podcast",{"title":148,"path":149,"stem":150},"How to Transcribe Video to Text Using OpenAI Whisper","/blog/music-data/how-to-transcribe-video-to-text-using-whisper","blog/music-data/how-to-transcribe-video-to-text-using-whisper",{"title":152,"path":153,"stem":154},"Epidemic Sound MCP with Claude for Devs","/blog/music-data/how-to-use-epidemic-sound-mcp-with-claude","blog/music-data/how-to-use-epidemic-sound-mcp-with-claude",{"title":156,"path":157,"stem":158},"Hybrid Database Model in Django for Speed","/blog/music-data/hybrid-database-model-in-django-as-a-performance-booster","blog/music-data/hybrid-database-model-in-django-as-a-performance-booster",{"title":160,"path":161,"stem":162},"Introduction to generating DDEX file using Python","/blog/music-data/introduction-to-generating-ddex-file-using-python","blog/music-data/introduction-to-generating-ddex-file-using-python",{"title":164,"path":165,"stem":166},"Maintaining Music Tech Tools: The SLA Dilemma for Small Teams","/blog/music-data/maintaining-music-tech-tools-the-sla-dilemma-for-small-teams","blog/music-data/maintaining-music-tech-tools-the-sla-dilemma-for-small-teams",{"title":168,"path":169,"stem":170},"Querying Bandcamp Revenue Reports with Natural Language — Meet mtl-bandcamp-mcp","/blog/music-data/mtl-bandcamp-mcp-open-source-revenue-dashboard","blog/music-data/mtl-bandcamp-mcp-open-source-revenue-dashboard",{"title":172,"path":173,"stem":174},"mtl-metadata-mcp: Open Source Audio Metadata Embedding for Claude Code","/blog/music-data/mtl-metadata-mcp-open-source-audio-metadata-embedding","blog/music-data/mtl-metadata-mcp-open-source-audio-metadata-embedding",{"title":176,"path":177,"stem":178},"MusicTech Resources for Builders","/blog/music-data/musictech-resources-curated-insights-for-the-musictech-builders","blog/music-data/musictech-resources-curated-insights-for-the-musictech-builders",{"title":180,"path":181,"stem":182},"Poland's Creative Tech and MusicTech Rise","/blog/music-data/polands-creative-tech-sector-is-on-the-rise-and-musictech-is-part-of-it","blog/music-data/polands-creative-tech-sector-is-on-the-rise-and-musictech-is-part-of-it",{"title":184,"path":185,"stem":186},"Batch ISRC Enrichment That Turns Messy Catalogs Into Clean Data","/blog/music-data/scout-isrc-metadata-enrichment-spotify-musicbrainz","blog/music-data/scout-isrc-metadata-enrichment-spotify-musicbrainz",{"title":188,"path":189,"stem":190},"Music Self-Publishing: The Emuze.me Story","/blog/music-data/self-publishing-in-the-music-industry-a-tale-of-emuze-me","blog/music-data/self-publishing-in-the-music-industry-a-tale-of-emuze-me",{"title":192,"path":193,"stem":194},"Understanding the API First Approach","/blog/music-data/understanding-the-api-first-approach","blog/music-data/understanding-the-api-first-approach",{"title":196,"path":197,"stem":198},"The Voice Memo Graveyard Problem","/blog/music-data/voice-memo-graveyard-problem","blog/music-data/voice-memo-graveyard-problem",{"title":200,"path":201,"stem":202},"Which Database for Music Data? Redshift vs BigQuery vs ClickHouse and When to Use Each","/blog/music-data/which-database-for-music-data-redshift-vs-bigquery-vs-clickhouse","blog/music-data/which-database-for-music-data-redshift-vs-bigquery-vs-clickhouse",{"title":204,"path":205,"stem":206},"Why we decided to use wavesurfer.js","/blog/music-data/why-we-decided-to-use-wavesurfer","blog/music-data/why-we-decided-to-use-wavesurfer",{"title":208,"path":209,"stem":210,"children":211,"page":69},"Newsletter","/blog/newsletter","blog/newsletter",[212,216,220,224,228,232,236,240,244,248,252,256,260,264,268,272,276,280,284,288,292,296,300,304,308],{"title":213,"path":214,"stem":215},"Music Industry Tech Openings (April 2024 Update)","/blog/newsletter/music-industry-tech-openings-april-2024-update","blog/newsletter/music-industry-tech-openings-april-2024-update",{"title":217,"path":218,"stem":219},"Music Industry Tech Openings (April 2025 Update)","/blog/newsletter/music-industry-tech-openings-april-2025-update","blog/newsletter/music-industry-tech-openings-april-2025-update",{"title":221,"path":222,"stem":223},"Music Industry Tech Openings (August 2024 Update)","/blog/newsletter/music-industry-tech-openings-august-2024-update","blog/newsletter/music-industry-tech-openings-august-2024-update",{"title":225,"path":226,"stem":227},"Music Industry Tech Openings (December 2024 Update)","/blog/newsletter/music-industry-tech-openings-december-2024-update","blog/newsletter/music-industry-tech-openings-december-2024-update",{"title":229,"path":230,"stem":231},"Music Industry Tech Openings (February 2025 Update)","/blog/newsletter/music-industry-tech-openings-february-2025-update","blog/newsletter/music-industry-tech-openings-february-2025-update",{"title":233,"path":234,"stem":235},"Music Industry Tech Openings (January 2025 Update)","/blog/newsletter/music-industry-tech-openings-january-2025-update","blog/newsletter/music-industry-tech-openings-january-2025-update",{"title":237,"path":238,"stem":239},"Music Industry Tech Openings (July 2024 Update)","/blog/newsletter/music-industry-tech-openings-july-2024-update","blog/newsletter/music-industry-tech-openings-july-2024-update",{"title":241,"path":242,"stem":243},"Music Industry Tech Openings (June 2024 Update)","/blog/newsletter/music-industry-tech-openings-june-2024-update","blog/newsletter/music-industry-tech-openings-june-2024-update",{"title":245,"path":246,"stem":247},"Music Industry Tech Openings (March 2025 Update)","/blog/newsletter/music-industry-tech-openings-march-2025-update","blog/newsletter/music-industry-tech-openings-march-2025-update",{"title":249,"path":250,"stem":251},"Music Industry Tech Openings (May 2024 Update)","/blog/newsletter/music-industry-tech-openings-may-2024-update","blog/newsletter/music-industry-tech-openings-may-2024-update",{"title":253,"path":254,"stem":255},"Music Industry Tech Openings (May 2025 Update)","/blog/newsletter/music-industry-tech-openings-may-2025","blog/newsletter/music-industry-tech-openings-may-2025",{"title":257,"path":258,"stem":259},"Music Industry Tech Openings (November 2024 Update)","/blog/newsletter/music-industry-tech-openings-november-2024-update","blog/newsletter/music-industry-tech-openings-november-2024-update",{"title":261,"path":262,"stem":263},"Music Industry Tech Openings (October 2024 Update)","/blog/newsletter/music-industry-tech-openings-october-2024-update","blog/newsletter/music-industry-tech-openings-october-2024-update",{"title":265,"path":266,"stem":267},"Music Industry Tech Openings (September 2024 Update)","/blog/newsletter/music-industry-tech-openings-september-2024-update","blog/newsletter/music-industry-tech-openings-september-2024-update",{"title":269,"path":270,"stem":271},"MusicTech Insights #1 by Maciej Dulski","/blog/newsletter/musictech-insights-1-curated-by-maciej-dulski","blog/newsletter/musictech-insights-1-curated-by-maciej-dulski",{"title":273,"path":274,"stem":275},"Provenance, Not Detection, Is the Durable Answer","/blog/newsletter/musictech-insights-10-curated-by-jim-anderson","blog/newsletter/musictech-insights-10-curated-by-jim-anderson",{"title":277,"path":278,"stem":279},"What CMOs can teach us about innovation in uncertain times","/blog/newsletter/musictech-insights-10-curated-by-joanna-kurkowska","blog/newsletter/musictech-insights-10-curated-by-joanna-kurkowska",{"title":281,"path":282,"stem":283},"Feeling the MusicTech Momentum","/blog/newsletter/musictech-insights-2-curated-by-maciej-dulski","blog/newsletter/musictech-insights-2-curated-by-maciej-dulski",{"title":285,"path":286,"stem":287},"AI in Music: Hype, Hope, and a Human Touch","/blog/newsletter/musictech-insights-3-curated-by-drew-thurlow","blog/newsletter/musictech-insights-3-curated-by-drew-thurlow",{"title":289,"path":290,"stem":291},"The Music Metadata Conundrum","/blog/newsletter/musictech-insights-4-curated-by-amanda-schupf","blog/newsletter/musictech-insights-4-curated-by-amanda-schupf",{"title":293,"path":294,"stem":295},"7 Rounds in the First 10 Days of November 2025","/blog/newsletter/musictech-insights-5-curated-by-maciej-dulski","blog/newsletter/musictech-insights-5-curated-by-maciej-dulski",{"title":297,"path":298,"stem":299},"The End of an Era: It's All About to Crash","/blog/newsletter/musictech-insights-6-curated-by-sigurdur-arnason","blog/newsletter/musictech-insights-6-curated-by-sigurdur-arnason",{"title":301,"path":302,"stem":303},"Low-Code Magic Won't Solve MusicTech Reality","/blog/newsletter/musictech-insights-7-curated-by-mariusz-smenzyk","blog/newsletter/musictech-insights-7-curated-by-mariusz-smenzyk",{"title":305,"path":306,"stem":307},"The New Economics of Game Music","/blog/newsletter/musictech-insights-8-curated-by-kenny-vaughan","blog/newsletter/musictech-insights-8-curated-by-kenny-vaughan",{"title":309,"path":310,"stem":311},"Music Business Meets Direct-to-Fan","/blog/newsletter/musictech-insights-9-curated-by-yaw-asamani","blog/newsletter/musictech-insights-9-curated-by-yaw-asamani",{"title":313,"path":314,"stem":315,"children":316,"page":69},"Software Development","/blog/software-development","blog/software-development",[317,321,325,329,333,337,341,345,349,353,357,361,365,369,373,377,381,385,389,393,397,401,405,409,413,417,421,425,429,433,437,441,445,449,453,457,461,465,469,473,477,481,485,489,493,497,501,505,509,513,517,521,525,529,533,537,541,545,549,553,557,561,565,569,573,577,581,585,589,593,597,601,605,609,613,617,621,625,629,633,637,641,645,649,653,657,661,665,669,673,677,681,685,689,693,697,701,705,709,713,717],{"title":318,"path":319,"stem":320},"Benefits of Outsourcing Software Development","/blog/software-development/10-benefits-of-outsourcing-software-development-services","blog/software-development/10-benefits-of-outsourcing-software-development-services",{"title":322,"path":323,"stem":324},"10 Steps to Find the Best MVP Developers","/blog/software-development/10-steps-to-find-the-best-mvp-developers-for-your-startup-idea","blog/software-development/10-steps-to-find-the-best-mvp-developers-for-your-startup-idea",{"title":326,"path":327,"stem":328},"1,200 Looms Later: How Async Video Became My Development Superpower","/blog/software-development/1200-looms-how-async-video-became-our-development-superpower","blog/software-development/1200-looms-how-async-video-became-our-development-superpower",{"title":330,"path":331,"stem":332},"Communication Strategy in Outsourcing Projects","/blog/software-development/5-steps-to-implement-an-effective-communication-strategy-in-outsourcing-software-development-project","blog/software-development/5-steps-to-implement-an-effective-communication-strategy-in-outsourcing-software-development-project",{"title":334,"path":335,"stem":336},"7 Best Practices for Outsourcing Software Development","/blog/software-development/7-best-practices-for-outsourcing-software-development","blog/software-development/7-best-practices-for-outsourcing-software-development",{"title":338,"path":339,"stem":340},"9 Reasons Why Saleor.io Is Best for eCommerce","/blog/software-development/9-reasons-why-the-saleor-io-platform-is-the-best-choice-for-your-ecommerce-website","blog/software-development/9-reasons-why-the-saleor-io-platform-is-the-best-choice-for-your-ecommerce-website",{"title":342,"path":343,"stem":344},"A Look at Bravelab.io’s Clutch 2021 Year In Review","/blog/software-development/a-look-at-bravelab-ios-clutch-2021-year-in-review","blog/software-development/a-look-at-bravelab-ios-clutch-2021-year-in-review",{"title":346,"path":347,"stem":348},"A quick introduction to profit sharing implementation","/blog/software-development/a-quick-introduction-to-profit-sharing-implementation","blog/software-development/a-quick-introduction-to-profit-sharing-implementation",{"title":350,"path":351,"stem":352},"AI Audio Similarity Search: The Future of Sound Library Discovery","/blog/software-development/ai-audio-similarity-search-for-sound-libraries","blog/software-development/ai-audio-similarity-search-for-sound-libraries",{"title":354,"path":355,"stem":356},"Automate Repetitive Tasks for Better Results","/blog/software-development/automate-repetitive-tasks-to-improve-your-business-performance","blog/software-development/automate-repetitive-tasks-to-improve-your-business-performance",{"title":358,"path":359,"stem":360},"Automating Success: The Art of Unified Documentation","/blog/software-development/automating-success-the-art-of-unified-documentation","blog/software-development/automating-success-the-art-of-unified-documentation",{"title":362,"path":363,"stem":364},"Brave 3.0 Website Redesign, Part 2: Solution","/blog/software-development/brave-3-0-how-we-conducted-website-redesign-part-2-solution","blog/software-development/brave-3-0-how-we-conducted-website-redesign-part-2-solution",{"title":366,"path":367,"stem":368},"Brave 3.0, Part 4: Tech Stack and Recap","/blog/software-development/brave-3-0-part-4-technologies-behind-and-final-series-recap","blog/software-development/brave-3-0-part-4-technologies-behind-and-final-series-recap",{"title":370,"path":371,"stem":372},"Brave 3.0 – redesign process part 1. The Challenge","/blog/software-development/brave-3-0-redesign-process-part-1-challenge","blog/software-development/brave-3-0-redesign-process-part-1-challenge",{"title":374,"path":375,"stem":376},"Brave 3.0 – redesign process, part 3. Lesson learned","/blog/software-development/brave-3-0-redesign-process-part-3-lesson-learned","blog/software-development/brave-3-0-redesign-process-part-3-lesson-learned",{"title":378,"path":379,"stem":380},"Bravelab.io: Top Software Developer by Clutch","/blog/software-development/bravelab-io-is-recognized-as-a-top-custom-software-developer-by-clutch","blog/software-development/bravelab-io-is-recognized-as-a-top-custom-software-developer-by-clutch",{"title":382,"path":383,"stem":384},"Bravelab.io: Top Developer in Poland by Clutch","/blog/software-development/bravelab-io-named-top-software-developer-in-poland-by-clutch","blog/software-development/bravelab-io-named-top-software-developer-in-poland-by-clutch",{"title":386,"path":387,"stem":388},"MusicTech Lab Partners with LALAL.AI","/blog/software-development/bravelab-partners-with-the-audio-lalal-ai","blog/software-development/bravelab-partners-with-the-audio-lalal-ai",{"title":390,"path":391,"stem":392},"MusicTech Lab Partners with The Audio Programmer","/blog/software-development/bravelab-partners-with-the-audio-programmer","blog/software-development/bravelab-partners-with-the-audio-programmer",{"title":394,"path":395,"stem":396},"Bravelab's team about productivity","/blog/software-development/bravelabs-team-about-productivity","blog/software-development/bravelabs-team-about-productivity",{"title":398,"path":399,"stem":400},"Braveloper","/blog/software-development/braveloper","blog/software-development/braveloper",{"title":402,"path":403,"stem":404},"Bravely App: Boost Productivity with Django","/blog/software-development/bravely-app-how-to-be-more-productive-with-django-quick","blog/software-development/bravely-app-how-to-be-more-productive-with-django-quick",{"title":406,"path":407,"stem":408},"DIY MIDI Controller for Ableton with Arduino","/blog/software-development/building-a-diy-midi-controller-for-ableton-live-with-arduino","blog/software-development/building-a-diy-midi-controller-for-ableton-live-with-arduino",{"title":410,"path":411,"stem":412},"C2PA in Ableton: Making AI Music Provenance Visible Inside Your DAW","/blog/software-development/c2pa-in-ableton-max-for-live","blog/software-development/c2pa-in-ableton-max-for-live",{"title":414,"path":415,"stem":416},"Change Detection mechanism in Angular","/blog/software-development/change-detection-mechanism-in-angular","blog/software-development/change-detection-mechanism-in-angular",{"title":418,"path":419,"stem":420},"Communication Channels in Remote Work","/blog/software-development/comparison-of-the-communication-channels-in-remote-work","blog/software-development/comparison-of-the-communication-channels-in-remote-work",{"title":422,"path":423,"stem":424},"Connecting Your Max for Live Device to a Cloud API","/blog/software-development/connecting-your-max-for-live-device-to-a-cloud-api","blog/software-development/connecting-your-max-for-live-device-to-a-cloud-api",{"title":426,"path":427,"stem":428},"From Voice Memo to Studio: The Cross-Platform Problem for Creators","/blog/software-development/cross-platform-problem-for-creators","blog/software-development/cross-platform-problem-for-creators",{"title":430,"path":431,"stem":432},"Cultural transformation through the pandemic era","/blog/software-development/cultural-transformation-through-the-pandemic-era","blog/software-development/cultural-transformation-through-the-pandemic-era",{"title":434,"path":435,"stem":436},"D-Commerce Decoded: Cutting Through the Hype","/blog/software-development/d-commerce-decoded-cutting-through-the-hype","blog/software-development/d-commerce-decoded-cutting-through-the-hype",{"title":438,"path":439,"stem":440},"Dev Meeting 002: Intro to DDD","/blog/software-development/dev-meeting-002-introduction-to-domain-driven-design-ddd","blog/software-development/dev-meeting-002-introduction-to-domain-driven-design-ddd",{"title":442,"path":443,"stem":444},"Dev Meeting 003: Web3 Primer","/blog/software-development/dev-meeting-003-web3-primer","blog/software-development/dev-meeting-003-web3-primer",{"title":446,"path":447,"stem":448},"Dev Meeting 004: Introduction to Event Storming","/blog/software-development/dev-meeting-004-introduction-to-event-storming","blog/software-development/dev-meeting-004-introduction-to-event-storming",{"title":450,"path":451,"stem":452},"Dev Meeting 001: Kubernetes is a Framework","/blog/software-development/dev-meeting-kubernetes-is-a-framework","blog/software-development/dev-meeting-kubernetes-is-a-framework",{"title":454,"path":455,"stem":456},"Did You Know? 10 Developer Tips from Real Codebases","/blog/software-development/did-you-know-dev-tips-part-1","blog/software-development/did-you-know-dev-tips-part-1",{"title":458,"path":459,"stem":460},"10 Surprising MusicTech Facts (Part 2)","/blog/software-development/did-you-know-musictech-facts-part-2","blog/software-development/did-you-know-musictech-facts-part-2",{"title":462,"path":463,"stem":464},"Django-cms and GraphQL","/blog/software-development/django-cms-and-graphql","blog/software-development/django-cms-and-graphql",{"title":466,"path":467,"stem":468},"Does Zappa make it super easy?","/blog/software-development/does-zappa-make-it-super-easy","blog/software-development/does-zappa-make-it-super-easy",{"title":470,"path":471,"stem":472},"Establishing cooperation between Netlify and Bravelab","/blog/software-development/establishing-cooperation-between-netlify-and-bravelab","blog/software-development/establishing-cooperation-between-netlify-and-bravelab",{"title":474,"path":475,"stem":476},"Export Ableton Locators to JSON via Max for Live","/blog/software-development/exporting-ableton-live-locators-to-json-with-max-for-live","blog/software-development/exporting-ableton-live-locators-to-json-with-max-for-live",{"title":478,"path":479,"stem":480},"IT Outsourcing: Success and Failure Factors","/blog/software-development/factors-that-contribute-to-the-success-or-failure-of-an-it-outsourcing-project","blog/software-development/factors-that-contribute-to-the-success-or-failure-of-an-it-outsourcing-project",{"title":482,"path":483,"stem":484},"Flutter 2022 Strategy: Analyzing the Roadmap","/blog/software-development/flutter-strategy-for-2022-analyzing-the-new-flutter-roadmap","blog/software-development/flutter-strategy-for-2022-analyzing-the-new-flutter-roadmap",{"title":486,"path":487,"stem":488},"Git Better #1 — Commit Message Convention","/blog/software-development/git-better-1-see-more-with-a-commit-message-convention","blog/software-development/git-better-1-see-more-with-a-commit-message-convention",{"title":490,"path":491,"stem":492},"Hasura in action. How to use it with Django","/blog/software-development/hasura-in-action","blog/software-development/hasura-in-action",{"title":494,"path":495,"stem":496},"Holacracy why and where we are","/blog/software-development/holacracy-why-and-where-we-are","blog/software-development/holacracy-why-and-where-we-are",{"title":498,"path":499,"stem":500},"How does JavaScript work","/blog/software-development/how-does-javascript-work","blog/software-development/how-does-javascript-work",{"title":502,"path":503,"stem":504},"How important is good UX/UI design?","/blog/software-development/how-important-is-good-ux-ui-design","blog/software-development/how-important-is-good-ux-ui-design",{"title":506,"path":507,"stem":508},"How repetitive tasks impact your business","/blog/software-development/how-repetitive-tasks-impact-your-business","blog/software-development/how-repetitive-tasks-impact-your-business",{"title":510,"path":511,"stem":512},"Becoming a Vue.js Dev: Do Paid Trials Work?","/blog/software-development/how-to-become-a-vue-js-developer-and-whether-paid-trials-in-it-work-out","blog/software-development/how-to-become-a-vue-js-developer-and-whether-paid-trials-in-it-work-out",{"title":514,"path":515,"stem":516},"How to Build an MVP in 6 Steps","/blog/software-development/how-to-build-a-minimum-viable-product-mvp-in-6-steps","blog/software-development/how-to-build-a-minimum-viable-product-mvp-in-6-steps",{"title":518,"path":519,"stem":520},"How to conduct workshops for creative industry?","/blog/software-development/how-to-conduct-workshops-for-creative-industry","blog/software-development/how-to-conduct-workshops-for-creative-industry",{"title":522,"path":523,"stem":524},"How to easily create form in Angular","/blog/software-development/how-to-easily-create-form-in-angular","blog/software-development/how-to-easily-create-form-in-angular",{"title":526,"path":527,"stem":528},"How to export orders in Saleor.io to XLSX file","/blog/software-development/how-to-export-orders-in-saleor-io-to-xlsx-file","blog/software-development/how-to-export-orders-in-saleor-io-to-xlsx-file",{"title":530,"path":531,"stem":532},"Handling High Loads on E-Commerce Platforms","/blog/software-development/how-to-handle-high-loads-on-e-commerce-platform-with-ease","blog/software-development/how-to-handle-high-loads-on-e-commerce-platform-with-ease",{"title":534,"path":535,"stem":536},"How to launch Saleor.io shop instance within 40h","/blog/software-development/how-to-launch-saleor-io-shop-instance-within-40h","blog/software-development/how-to-launch-saleor-io-shop-instance-within-40h",{"title":538,"path":539,"stem":540},"First Steps to Build a Business Relationship","/blog/software-development/how-to-make-the-first-step-to-establish-a-business-relationship","blog/software-development/how-to-make-the-first-step-to-establish-a-business-relationship",{"title":542,"path":543,"stem":544},"Multi-Tenant Apps with Django and Saleor.io","/blog/software-development/how-to-manage-tenants-in-the-multitenant-app-based-on-django-tenants-and-saleor-io-platform","blog/software-development/how-to-manage-tenants-in-the-multitenant-app-based-on-django-tenants-and-saleor-io-platform",{"title":546,"path":547,"stem":548},"Notion Backup Tool Built in 3 Days with Python","/blog/software-development/how-we-built-a-notion-backup-tool-in-3-days-with-pythonvue-and-why","blog/software-development/how-we-built-a-notion-backup-tool-in-3-days-with-pythonvue-and-why",{"title":550,"path":551,"stem":552},"Important new features in Python 3.8","/blog/software-development/important-new-features-in-python-3-8","blog/software-development/important-new-features-in-python-3-8",{"title":554,"path":555,"stem":556},"Installing Proxmox on dedicated server from OVH","/blog/software-development/installing-proxmox-on-dedicated-server-from-ovh","blog/software-development/installing-proxmox-on-dedicated-server-from-ovh",{"title":558,"path":559,"stem":560},"Integrating SignNow E-Signatures into Your Django Application","/blog/software-development/integrating-signnow-e-signatures-into-your-django-application","blog/software-development/integrating-signnow-e-signatures-into-your-django-application",{"title":562,"path":563,"stem":564},"Tempus Metronome and GetSongBPM API","/blog/software-development/integrating-tempus-metronome-with-the-getsongbpm-api-what-bpm-really-means-and-how-to-use-it","blog/software-development/integrating-tempus-metronome-with-the-getsongbpm-api-what-bpm-really-means-and-how-to-use-it",{"title":566,"path":567,"stem":568},"Introducing MusicTech Poland","/blog/software-development/introducing-musictech-poland","blog/software-development/introducing-musictech-poland",{"title":570,"path":571,"stem":572},"Vue.js as a Frontend for Saleor.io","/blog/software-development/is-it-possible-to-use-vue-js-as-a-frontend-for-saleor-io-platform","blog/software-development/is-it-possible-to-use-vue-js-as-a-frontend-for-saleor-io-platform",{"title":574,"path":575,"stem":576},"Is your business ready for the cashless era?","/blog/software-development/is-your-business-ready-for-the-cashless-era","blog/software-development/is-your-business-ready-for-the-cashless-era",{"title":578,"path":579,"stem":580},"Is your face ready to buy?","/blog/software-development/is-your-face-ready-to-buy","blog/software-development/is-your-face-ready-to-buy",{"title":582,"path":583,"stem":584},"JS Frameworks: Trends and Opportunities","/blog/software-development/javascript-trending-frameworks-and-market-opportunities","blog/software-development/javascript-trending-frameworks-and-market-opportunities",{"title":586,"path":587,"stem":588},"Kanban Board: Boost Your Team Productivity","/blog/software-development/kanban-board-methodology-hack-your-companys-productivity","blog/software-development/kanban-board-methodology-hack-your-companys-productivity",{"title":590,"path":591,"stem":592},"Verified Human Cert MCP Server: Prove Your Music Is Human-Made, Right from the Terminal","/blog/software-development/mcp-verified-human-cert-open-source","blog/software-development/mcp-verified-human-cert-open-source",{"title":594,"path":595,"stem":596},"Migrating from TravisCI to Github Actions","/blog/software-development/migrating-from-travisci-to-github-actions","blog/software-development/migrating-from-travisci-to-github-actions",{"title":598,"path":599,"stem":600},"MusicTech Lab: Top Software Developer by Clutch","/blog/software-development/musictechlab-is-recognized-as-a-top-custom-software-developer-by-clutch","blog/software-development/musictechlab-is-recognized-as-a-top-custom-software-developer-by-clutch",{"title":602,"path":603,"stem":604},"MusicTech Lab x Verified Human: Building a Trust Layer for Human-Made Music","/blog/software-development/musictechlab_blog_verified_human_partnership","blog/software-development/musictechlab_blog_verified_human_partnership",{"title":606,"path":607,"stem":608},"MusicXML: Standard for Music Notation","/blog/software-development/musicxml-standard-for-music-notation-and-education","blog/software-development/musicxml-standard-for-music-notation-and-education",{"title":610,"path":611,"stem":612},"Only a few books but dozens of ideas","/blog/software-development/only-a-few-books-but-dozens-of-ideas","blog/software-development/only-a-few-books-but-dozens-of-ideas",{"title":614,"path":615,"stem":616},"Overdue Invoices and Issue Tracker Integration","/blog/software-development/overdue-invoices-integration-with-the-issue-tracking-system","blog/software-development/overdue-invoices-integration-with-the-issue-tracking-system",{"title":618,"path":619,"stem":620},"Performing SAML SSO using JWT in Django","/blog/software-development/performing-saml-sso-using-jwt-in-django","blog/software-development/performing-saml-sso-using-jwt-in-django",{"title":622,"path":623,"stem":624},"Progressive Web Apps for Mobile Development","/blog/software-development/progressive-web-apps-a-new-way-of-creating-mobile-application","blog/software-development/progressive-web-apps-a-new-way-of-creating-mobile-application",{"title":626,"path":627,"stem":628},"Recruitment System: Gmail, Jira, and CRM","/blog/software-development/recruitment-system-integrating-gmail-bravely-jira-slack-and-copper-crm","blog/software-development/recruitment-system-integrating-gmail-bravely-jira-slack-and-copper-crm",{"title":630,"path":631,"stem":632},"Scratch Me: Chrome Extension for Leads","/blog/software-development/scratch-me-a-simple-chrome-extension-which-will-increase-your-productivity","blog/software-development/scratch-me-a-simple-chrome-extension-which-will-increase-your-productivity",{"title":634,"path":635,"stem":636},"Scratch Me – integration with the Copper CRM","/blog/software-development/scratch-me-integration-with-the-copper-crm","blog/software-development/scratch-me-integration-with-the-copper-crm",{"title":638,"path":639,"stem":640},"SignNow MCP Server: E-Signatures Straight from Claude Code","/blog/software-development/signnow-mcp-server-e-signatures-from-claude-code","blog/software-development/signnow-mcp-server-e-signatures-from-claude-code",{"title":642,"path":643,"stem":644},"Music Industry Tech Openings (March 2024 Update)","/blog/software-development/technical-job-opportunities-in-the-music-industry","blog/software-development/technical-job-opportunities-in-the-music-industry",{"title":646,"path":647,"stem":648},"Thanks app – a Management 3.0 solution","/blog/software-development/thanks-app-a-management-3-0-solution","blog/software-development/thanks-app-a-management-3-0-solution",{"title":650,"path":651,"stem":652},"Colonial Pipeline Case: 7 Security Reminders","/blog/software-development/the-case-of-colonial-pipeline-and-7-security-reminders","blog/software-development/the-case-of-colonial-pipeline-and-7-security-reminders",{"title":654,"path":655,"stem":656},"The Evolution and Future of E-commerce Platforms","/blog/software-development/the-evolution-and-future-of-e-commerce-platforms","blog/software-development/the-evolution-and-future-of-e-commerce-platforms",{"title":658,"path":659,"stem":660},"The Gender Gap in the Tech Industry","/blog/software-development/the-gender-gap-in-the-tech-industry","blog/software-development/the-gender-gap-in-the-tech-industry",{"title":662,"path":663,"stem":664},"First Attempt to Implement 4DX at Bravelab.io","/blog/software-development/the-very-first-attempt-to-implement-4dx-in-bravelab-io","blog/software-development/the-very-first-attempt-to-implement-4dx-in-bravelab-io",{"title":666,"path":667,"stem":668},"The WTF Scale: IT Project Complexity","/blog/software-development/the-wtf-programming-scale-measuring-it-project-complexity","blog/software-development/the-wtf-programming-scale-measuring-it-project-complexity",{"title":670,"path":671,"stem":672},"Top 10 articles through the eyes of our developers","/blog/software-development/top-10-articles-through-the-eyes-of-our-developers","blog/software-development/top-10-articles-through-the-eyes-of-our-developers",{"title":674,"path":675,"stem":676},"Top 6 apps made with Flutter","/blog/software-development/top-6-apps-made-with-flutter","blog/software-development/top-6-apps-made-with-flutter",{"title":678,"path":679,"stem":680},"Uber 101: How Uber Made It to the Top","/blog/software-development/uber-101-how-this-ride-sharing-behemoth-made-it-to-the-top","blog/software-development/uber-101-how-this-ride-sharing-behemoth-made-it-to-the-top",{"title":682,"path":683,"stem":684},"MusicTech Lab Partners with Music Glue","/blog/software-development/unifying-artists-and-audiences-exploring-music-glue","blog/software-development/unifying-artists-and-audiences-exploring-music-glue",{"title":686,"path":687,"stem":688},"Why AI Will Defeat Traditional HR","/blog/software-development/warning-why-artificial-intelligence-will-defeat-traditional-hr","blog/software-development/warning-why-artificial-intelligence-will-defeat-traditional-hr",{"title":690,"path":691,"stem":692},"What is a Discovery Document?","/blog/software-development/what-is-discovery-document","blog/software-development/what-is-discovery-document",{"title":694,"path":695,"stem":696},"What is Flutter, and Why is it Worth Considering?","/blog/software-development/what-is-flutter-and-why-is-it-worth-considering","blog/software-development/what-is-flutter-and-why-is-it-worth-considering",{"title":698,"path":699,"stem":700},"What is a Watermarked Song?","/blog/software-development/what-is-watermarked-song","blog/software-development/what-is-watermarked-song",{"title":702,"path":703,"stem":704},"Choosing a Frontend Framework for the Web","/blog/software-development/which-framework-should-you-choose-for-the-frontend-web-platform-development","blog/software-development/which-framework-should-you-choose-for-the-frontend-web-platform-development",{"title":706,"path":707,"stem":708},"Why DAWs Are the Wrong Tool for Starting a Song","/blog/software-development/why-daws-wrong-tool-for-starting-song","blog/software-development/why-daws-wrong-tool-for-starting-song",{"title":710,"path":711,"stem":712},"Why the Programming World Loves Python","/blog/software-development/why-the-programming-world-loves-python","blog/software-development/why-the-programming-world-loves-python",{"title":714,"path":715,"stem":716},"Why We Don't Build Chat From Scratch (And Neither Should You)","/blog/software-development/why-we-dont-build-chat-from-scratch","blog/software-development/why-we-dont-build-chat-from-scratch",{"title":718,"path":719,"stem":720},"Why we use Sanity.io","/blog/software-development/why-we-use-sanity-io","blog/software-development/why-we-use-sanity-io",{"title":722,"path":723,"stem":724,"children":725,"page":69},"Sportstech","/blog/sportstech","blog/sportstech",[726,730],{"title":727,"path":728,"stem":729},"BeatBuddy Replay: Video Analysis App Challenges","/blog/sportstech/beatbuddy-replay-video-analysis-app-for-swimmers-flutter","blog/sportstech/beatbuddy-replay-video-analysis-app-for-swimmers-flutter",{"title":731,"path":732,"stem":733},"How to Create a Watch Face App for Garmin Watch","/blog/sportstech/how-to-create-watch-face-app-for-garmin-watch","blog/sportstech/how-to-create-watch-face-app-for-garmin-watch",{"id":735,"title":54,"authors":736,"badge":737,"body":739,"category":897,"client":898,"date":900,"description":901,"extension":902,"faq":736,"featured":69,"featuredOrder":736,"hidden":69,"image":903,"keyTakeaways":905,"meta":917,"navigation":906,"path":55,"seo":918,"status":736,"stem":56,"tags":919,"teaser":736,"__hash__":921},"posts/blog/case-study/ticketing-events-platform-case-study.md",null,{"label":5,"color":738},"#E91E63",{"type":740,"value":741,"toc":886},"minimark",[742,746,749,754,776,780,783,786,792,796,802,809,813,818,824,833,837,840,844,866,870],[743,744,745],"p",{},"Solidstudio needed developers with a specific skill set. Their client was building a ticketing platform and needed to integrate the core product with multiple third-party providers through dedicated APIs.",[743,747,748],{},"The internal team lacked engineers who could handle this work — but the integrations were a key revenue driver.",[750,751,753],"h2",{"id":752},"technologies-used","Technologies Used",[755,756,763,768,772],"div",{"className":757},[758,759,760,761,762],"grid","grid-cols-1","md:grid-cols-3","gap-4","my-8",[764,765],"spotlight-card",{"description":766,"title":767},"Backend development","Python 3",[764,769],{"description":770,"title":771},"Asynchronous programming","asyncio",[764,773],{"description":774,"title":775},"Modern Python web framework","FastAPI",[750,777,779],{"id":778},"how-we-worked","How We Worked",[743,781,782],{},"Our engineers joined the client's in-house team. They participated in standups, used the company chat, and shared availability — but their focus stayed on one thing: building integrations with external partners.",[743,784,785],{},"When our team joined, there were zero external integrations. Within months, they delivered over a dozen — each one directly translating into new revenue for the client.",[787,788,789],"tip",{},[743,790,791],{},"The client was positively surprised by the velocity. Working solutions were delivered within weeks of starting.",[750,793,795],{"id":794},"team-testimonial","Team Testimonial",[797,798,799],"blockquote",{},[743,800,801],{},"Since joining the project, I have had an excellent opportunity to grow. I was engaged for more than one year — the best learning period of my career. I was changing alongside the project, and it was a joy to participate in it.",[743,803,804,808],{},[805,806,807],"strong",{},"Szymon Zmilczak"," — Python Senior Developer",[750,810,812],{"id":811},"client-testimonial","Client Testimonial",[797,814,815],{},[743,816,817],{},"I trust their developers with all the technical decisions they make because they're very senior. I'm very happy with their ability to adjust to our needs. They are always responsive, collaborative, and adhere well to any budgetary or timescale requirements.",[743,819,820,823],{},[805,821,822],{},"Paweł Małkowiak"," — Co-Founder, Solidstudio",[743,825,826],{},[827,828,832],"a",{"href":829,"rel":830},"https://clutch.co/profile/musictech-lab#review-1066514",[831],"nofollow","See full review on Clutch",[750,834,836],{"id":835},"outcome","Outcome",[743,838,839],{},"The first project was handled entirely by our team. It's live, being used by the end client, and user feedback has been very positive. For the follow-up project, our developer met the initial requirements and became a trusted part of the team.",[750,841,843],{"id":842},"project-details","Project Details",[845,846,847,854,860],"ul",{},[848,849,850,853],"li",{},[805,851,852],{},"Duration:"," 24+ months",[848,855,856,859],{},[805,857,858],{},"Services:"," Backend Development, API Integration, IT Staff Augmentation",[848,861,862,865],{},[805,863,864],{},"Team:"," Oleksandr, Paweł, Jakub, Szymon — Python Developers",[750,867,869],{"id":868},"related-articles","Related Articles",[845,871,872,877,882],{},[848,873,874],{},[827,875,876],{"href":319},"10 Benefits of outsourcing software development services",[848,878,879],{},[827,880,881],{"href":331},"5 steps to implement an effective communication strategy in outsourcing software development projects",[848,883,884],{},[827,885,334],{"href":335},{"title":887,"searchDepth":888,"depth":888,"links":889},"",2,[890,891,892,893,894,895,896],{"id":752,"depth":888,"text":753},{"id":778,"depth":888,"text":779},{"id":794,"depth":888,"text":795},{"id":811,"depth":888,"text":812},{"id":835,"depth":888,"text":836},{"id":842,"depth":888,"text":843},{"id":868,"depth":888,"text":869},"case-study",{"name":899},"Solidstudio","2022-01-01T00:00:00.000Z","How we provided a Python development team to build third-party API integrations for a ticketing platform, through long-term team augmentation.","md",{"src":904},"/images/cdn-migrated/bravelab_experience_solidstudio.webp",{"enabled":906,"items":907},true,[908,911,914],{"text":909,"icon":910},"Over a dozen third-party API integrations delivered from zero within months.","i-lucide-code",{"text":912,"icon":913},"24+ month team augmentation engagement with 4 Python developers.","i-lucide-handshake",{"text":915,"icon":916},"Each integration directly translated into new revenue for the client.","i-lucide-trending-up",{},{"title":54,"description":901},[897,920],"ecommerce","7q7_6IaklPlqwPe87srrU9NNva15SSSc6KAuLdMN7aA",[923,925],{"title":50,"path":51,"stem":52,"description":924,"children":-1},"How we augmented an HR startup's in-house team with frontend developers for Angular and React.js projects, delivering features on a tight deadline.",{"title":58,"path":59,"stem":60,"description":926,"children":-1},"How Roadie.co helps independent artists build websites, manage tours, release music, and grow their fanbase with data-driven tools and email marketing.",[928,1242,4538,4905],{"id":929,"title":62,"authors":736,"badge":930,"body":931,"category":897,"client":1218,"date":1221,"description":1222,"extension":902,"faq":736,"featured":69,"featuredOrder":736,"hidden":69,"image":1223,"keyTakeaways":1225,"meta":1236,"navigation":906,"path":1237,"seo":1238,"status":736,"stem":64,"tags":1239,"teaser":736,"__hash__":1240,"score":1241},"posts/blog/case-study/walkative-2-booking-platform-case-study.md",{"label":5,"color":738},{"type":740,"value":932,"toc":1208},[933,936,940,943,947,979,983,986,1006,1012,1016,1019,1023,1027,1061,1065,1103,1105,1110,1148,1150],[743,934,935],{},"Walkative 2.0 is a cloud-based booking platform for free walking tours. It automates the entire reservation process, handling thousands of weekly bookings across multiple European cities.",[750,937,939],{"id":938},"about-walkative","About Walkative",[743,941,942],{},"Founded in 2007 in Krakow by a group of passionate guides, Walkative now operates across Europe and beyond, offering free walking tours in dozens of cities. The platform serves as the central hub for managing tours, guides, partners, and reservations.",[750,944,946],{"id":945},"platform-components","Platform Components",[755,948,950,955,960,965,970,975],{"className":949},[758,759,760,761,762],[764,951],{"description":952,"icon":953,"title":954},"RESTful API architecture","i-lucide-server","Backend API",[764,956],{"description":957,"icon":958,"title":959},"iOS & Android for guides","i-lucide-smartphone","Mobile App",[764,961],{"description":962,"icon":963,"title":964},"Cards, wallets & tipping","i-lucide-credit-card","Stripe Payments",[764,966],{"description":967,"icon":968,"title":969},"TripAdvisor, Viator, GetYourGuide & more","i-lucide-globe","OTA Integrations",[764,971],{"description":972,"icon":973,"title":974},"Content management system","i-lucide-file-text","CMS",[764,976],{"description":977,"icon":910,"title":978},"Embeddable HTML/JS widget","Booking Widget",[750,980,982],{"id":981},"challenge","Challenge",[743,984,985],{},"Walkative needed a comprehensive booking platform to handle thousands of weekly reservations across multiple cities. The platform had to:",[845,987,988,991,994,997,1000,1003],{},[848,989,990],{},"Automate the entire reservation process for free walking tours",[848,992,993],{},"Support multiple user roles: Tourists, Guides, Partners, Managers, and Administrators",[848,995,996],{},"Integrate with major OTA platforms to prevent overbooking",[848,998,999],{},"Provide a mobile application for guides to manage their tours on the go",[848,1001,1002],{},"Offer a Direct Booking Widget that could be embedded on partner websites",[848,1004,1005],{},"Handle complex availability management with backup guide functionality",[1007,1008,1009],"note",{},[743,1010,1011],{},"The main challenge was creating a scalable system that could handle growing reservations while maintaining synchronization across multiple booking channels to prevent overbooking.",[750,1013,1015],{"id":1014},"solution","Solution",[743,1017,1018],{},"We developed Walkative 2.0 as a comprehensive cloud-based platform with the following key modules:",[1020,1021],"project-timeline",{":items":1022},"[{\"title\":\"Booking Module\",\"description\":\"Simple reservation flow with email confirmations, cancellation links, departure board showing all available tours, and support for up to 7 participants per booking.\",\"icon\":\"i-lucide-ticket\"},{\"title\":\"Availability Module\",\"description\":\"Centralized tour & guide management with time slots, automatic backup guide alerts at 70% capacity, dynamic limits, and calendar view filtered by city.\",\"icon\":\"i-lucide-calendar\"},{\"title\":\"Partners Module\",\"description\":\"Self-service dashboard for tourism companies to manage their own tours, reservations, and guide availability with tailored permissions.\",\"icon\":\"i-lucide-handshake\"},{\"title\":\"Direct Booking Widget\",\"description\":\"Embeddable HTML/JS widget with calendar-based booking, mobile-responsive design, and Google Ads enhanced conversion tracking.\",\"icon\":\"i-lucide-layout-template\"},{\"title\":\"Mobile App — Walkative Guide\",\"description\":\"Dedicated app for guides: view tours, manage participants, track tips, confirm attendance, request backup guides, and access settlements.\",\"icon\":\"i-lucide-smartphone\"},{\"title\":\"OTA Integration Engine\",\"description\":\"Real-time availability sync with TripAdvisor, Viator, GetYourGuide, GuruWalk, Civitatis, and FreeTour to prevent overbooking.\",\"icon\":\"i-lucide-refresh-cw\"},{\"title\":\"Payment Module\",\"description\":\"Stripe integration for credit/debit cards, digital wallets, post-tour payment links, and online tipping for guides.\",\"icon\":\"i-lucide-credit-card\"},{\"title\":\"Notification Module\",\"description\":\"Automated emails with guide photos, 12-hour reminders, SMS alerts for guides, push notifications, and overbooking warnings.\",\"icon\":\"i-lucide-bell\"}]",[750,1024,1026],{"id":1025},"additional-features","Additional Features",[755,1028,1032,1037,1041,1046,1051,1056],{"className":1029},[758,759,1030,1031,761,762],"md:grid-cols-2","lg:grid-cols-3",[764,1033],{"description":1034,"icon":1035,"title":1036},"Tourists, Guides, Partners, Managers, Administrators","i-lucide-users","Multi-Role System",[764,1038],{"description":1039,"icon":973,"title":1040},"Tours, categories, FAQ, and text pages","CMS Module",[764,1042],{"description":1043,"icon":1044,"title":1045},"Post-tour feedback system","i-lucide-star","Ratings & Comments",[764,1047],{"description":1048,"icon":1049,"title":1050},"Promo codes, QR codes, commission tracking","i-lucide-link","Affiliate Module",[764,1052],{"description":1053,"icon":1054,"title":1055},"Facebook, Google, Apple & email/password","i-lucide-log-in","Authentication",[764,1057],{"description":1058,"icon":1059,"title":1060},"Calendar, users, widgets, reports & settings","i-lucide-layout-dashboard","Admin Panel",[750,1062,1064],{"id":1063},"tech-stack","Tech Stack",[755,1066,1071,1074,1077,1081,1086,1089,1093,1098],{"className":1067},[758,1068,760,1069,1070,762],"grid-cols-2","lg:grid-cols-4","gap-3",[764,1072],{"description":1073,"icon":953,"title":954},"RESTful architecture",[764,1075],{"description":1076,"icon":958,"title":959},"iOS & Android",[764,1078],{"description":1079,"icon":963,"title":1080},"Payments & tipping","Stripe",[764,1082],{"description":1083,"icon":1084,"title":1085},"6 platform integrations","i-lucide-plug","OTA APIs",[764,1087],{"description":1088,"icon":973,"title":974},"Content management",[764,1090],{"description":1091,"icon":910,"title":1092},"HTML/JavaScript","Widget",[764,1094],{"description":1095,"icon":1096,"title":1097},"Email, SMS & Push","i-lucide-mail","Notifications",[764,1099],{"description":1100,"icon":1101,"title":1102},"Social & email login","i-lucide-shield","Auth",[750,1104,836],{"id":835},[787,1106,1107],{},[743,1108,1109],{},"The platform successfully handles thousands of weekly reservations and continues to scale as Walkative expands to new cities and partners.",[845,1111,1112,1118,1124,1130,1136,1142],{},[848,1113,1114,1117],{},[805,1115,1116],{},"Automated reservations"," — reduced manual work across all booking channels",[848,1119,1120,1123],{},[805,1121,1122],{},"Zero overbooking"," — real-time OTA synchronization keeps availability in sync",[848,1125,1126,1129],{},[805,1127,1128],{},"Partner self-service"," — tourism companies manage tours independently",[848,1131,1132,1135],{},[805,1133,1134],{},"Mobile-first guides"," — dedicated app for on-the-go tour management",[848,1137,1138,1141],{},[805,1139,1140],{},"Flexible embedding"," — Direct Booking Widget works on any website",[848,1143,1144,1147],{},[805,1145,1146],{},"GDPR compliant"," — full data security and privacy compliance",[750,1149,843],{"id":842},[1151,1152,1153,1164],"table",{},[1154,1155,1156],"thead",{},[1157,1158,1159,1162],"tr",{},[1160,1161],"th",{},[1160,1163],{},[1165,1166,1167,1178,1188,1198],"tbody",{},[1157,1168,1169,1175],{},[1170,1171,1172],"td",{},[805,1173,1174],{},"Project Duration",[1170,1176,1177],{},"12+ months",[1157,1179,1180,1185],{},[1170,1181,1182],{},[805,1183,1184],{},"Monthly Visitors",[1170,1186,1187],{},"1M+ (within 12 months)",[1157,1189,1190,1195],{},[1170,1191,1192],{},[805,1193,1194],{},"Booking Channels",[1170,1196,1197],{},"6+ OTA platforms + direct widget",[1157,1199,1200,1205],{},[1170,1201,1202],{},[805,1203,1204],{},"User Roles",[1170,1206,1207],{},"5 (Tourist, Guide, Partner, Manager, Admin)",{"title":887,"searchDepth":888,"depth":888,"links":1209},[1210,1211,1212,1213,1214,1215,1216,1217],{"id":938,"depth":888,"text":939},{"id":945,"depth":888,"text":946},{"id":981,"depth":888,"text":982},{"id":1014,"depth":888,"text":1015},{"id":1025,"depth":888,"text":1026},{"id":1063,"depth":888,"text":1064},{"id":835,"depth":888,"text":836},{"id":842,"depth":888,"text":843},{"name":1219,"logo":1220},"Walkative","/images/logos/walkative.webp","2025-06-01T00:00:00.000Z","How we built a cloud-based booking platform for the leading free walking tour company, handling thousands of weekly reservations across European cities.",{"src":1224,"hasLogo":906},"/images/case-studies/Walkative_MusicTech_Lab_Case_Study.webp",{"enabled":906,"items":1226},[1227,1229,1232,1234],{"text":1228,"icon":968},"Cloud booking platform handling thousands of weekly reservations across European cities.",{"text":1230,"icon":1231},"Real-time sync with 6+ OTA platforms like Viator and GetYourGuide prevents overbooking.","i-lucide-refresh-cw",{"text":1233,"icon":916},"1M+ monthly visitors within 12 months of launch.",{"text":1235,"icon":958},"Dedicated Flutter mobile app for guides with tips, settlements, and tour management.",{},"/blog/case-studies/walkative-2-booking-platform-case-study",{"title":62,"description":1222},[897,920],"x_ODpClQFHdokCOwWM6emLgxc0pEUWkaqevL2oJx-YU",4,{"id":1243,"title":26,"authors":1244,"badge":736,"body":1250,"category":897,"client":4511,"date":4514,"description":4515,"extension":902,"faq":736,"featured":69,"featuredOrder":736,"hidden":69,"image":4516,"keyTakeaways":4518,"meta":4532,"navigation":906,"path":4533,"seo":4534,"status":736,"stem":28,"tags":4535,"teaser":736,"__hash__":4537,"score":1542},"posts/blog/case-study/how-we-built-memosonic-accessible-audio-memory-game-flutter.md",[1245],{"name":1246,"to":1247,"avatar":1248},"Mariusz Smenżyk","https://www.linkedin.com/in/msmenzyk",{"src":1249},"/images/people/mariusz-smenzyk2.webp",{"type":740,"value":1251,"toc":4450},[1252,1256,1259,1266,1269,1291,1294,1298,1301,1321,1324,1329,1375,1377,1381,1385,1388,1399,1405,1409,1416,1419,1444,1447,1457,1459,1463,1466,1473,1478,1484,1488,1491,1511,1776,1786,1789,1795,1799,1805,2055,2061,2067,2073,2076,2382,2385,2389,2395,2634,2637,2641,2644,2650,2761,2767,2795,2801,2868,2872,2875,2956,2962,2966,2969,2995,3001,3003,3007,3010,3014,3030,3035,3040,3045,3050,3054,3059,3064,3066,3070,3073,3077,3080,3086,3100,3104,3107,3113,3115,3119,3122,3181,3195,3198,3201,3203,3207,3210,3214,3217,3222,3225,3239,3253,3257,3260,3265,3279,3284,3295,3300,3311,3316,3327,3329,3333,3337,3343,3347,3439,3443,3446,3675,3682,3684,3688,3692,3695,3699,3702,3713,3717,3720,3724,3727,3741,3744,3746,3750,3756,3759,3764,3775,3780,3791,3796,3807,3812,3815,3820,3828,3839,3842,3844,3848,3851,3856,3870,3874,3877,3891,3894,3900,3903,3907,3913,3923,3929,3935,3939,3942,3980,3983,3987,3990,3993,3996,4022,4026,4029,4034,4037,4043,4049,4054,4056,4060,4063,4096,4099,4101,4105,4108,4134,4137,4139,4143,4173,4175,4179,4182,4186,4195,4198,4255,4259,4265,4279,4285,4291,4295,4298,4301,4339,4342,4356,4360,4367,4370,4374,4377,4403,4410,4413,4415,4419,4446],[750,1253,1255],{"id":1254},"it-started-with-a-simple-game","It Started with a Simple Game",[743,1257,1258],{},"Picture this: a rainy Sunday afternoon, kids bouncing off the walls, and a parent desperately trying to find something educational yet fun. We pulled out the classic memory card game - flip two cards, find matching pairs.",[743,1260,1261,1262],{},"The kids loved it. But something clicked in my head: ",[1263,1264,1265],"em",{},"What if instead of matching pictures, we matched sounds?",[743,1267,1268],{},"That question sparked MemoSonic.",[755,1270,1276,1277,1276,1281,1276,1288],{"className":1271},[1272,1273,1274,1275],"flex","flex-row","items-center","justify-center","\n  ",[755,1278],{"className":1279},[1280],"w-1/4",[1282,1283],"img",{"src":1284,"alt":1285,"className":1286},"/images/blog/musictechlab_blog_how-we-built-memosonic_inline_0.webp","MemoSonic home screen",[1287],"w-1/2",[755,1289],{"className":1290},[1280],[1292,1293],"hr",{},[750,1295,1297],{"id":1296},"the-problem-visual-learning-isnt-everything","The Problem: Visual Learning Isn't Everything",[743,1299,1300],{},"Traditional memory games are purely visual. You see an image, remember its position, find its pair. Great for training visual memory, but what about:",[845,1302,1303,1309,1315],{},[848,1304,1305,1308],{},[805,1306,1307],{},"Auditory learners"," who process information better through sound?",[848,1310,1311,1314],{},[805,1312,1313],{},"Young musicians"," trying to recognize chords, scales, or instrument timbres?",[848,1316,1317,1320],{},[805,1318,1319],{},"Visually impaired children"," who can't participate in traditional memory games at all?",[743,1322,1323],{},"We realized there was a gap. A big one.",[1325,1326,1328],"h3",{"id":1327},"what-we-wanted-to-build","What We Wanted to Build:",[1151,1330,1331,1341],{},[1154,1332,1333],{},[1157,1334,1335,1338],{},[1160,1336,1337],{},"Traditional Memory",[1160,1339,1340],{},"MemoSonic",[1165,1342,1343,1351,1359,1367],{},[1157,1344,1345,1348],{},[1170,1346,1347],{},"Visual only",[1170,1349,1350],{},"Sound-first approach",[1157,1352,1353,1356],{},[1170,1354,1355],{},"Static images",[1170,1357,1358],{},"Interactive audio feedback",[1157,1360,1361,1364],{},[1170,1362,1363],{},"Limited accessibility",[1170,1365,1366],{},"Inclusive by design",[1157,1368,1369,1372],{},[1170,1370,1371],{},"One learning style",[1170,1373,1374],{},"Multiple categories for different interests",[1292,1376],{},[750,1378,1380],{"id":1379},"building-memosonic-from-idea-to-app","Building MemoSonic: From Idea to App",[1325,1382,1384],{"id":1383},"choosing-the-tech-stack","Choosing the Tech Stack",[743,1386,1387],{},"We needed something that would:",[845,1389,1390,1393,1396],{},[848,1391,1392],{},"Work on both iOS and Android",[848,1394,1395],{},"Handle audio playback smoothly",[848,1397,1398],{},"Feel responsive and polished",[743,1400,1401,1404],{},[805,1402,1403],{},"Flutter"," was the obvious choice. Cross-platform, beautiful animations out of the box, and a fantastic ecosystem for audio libraries.",[1325,1406,1408],{"id":1407},"the-audio-challenge","The Audio Challenge",[743,1410,1411,1412,1415],{},"Here's something most developers don't think about: ",[805,1413,1414],{},"audio is hard",".",[743,1417,1418],{},"Not the playback itself - that's straightforward. The hard part is:",[1420,1421,1422,1432,1438],"ol",{},[848,1423,1424,1427,1428,1431],{},[805,1425,1426],{},"Timing"," - When a player taps a card, the sound needs to play ",[1263,1429,1430],{},"instantly",". Even 100ms delay feels wrong.",[848,1433,1434,1437],{},[805,1435,1436],{},"Overlapping sounds"," - What happens when someone taps two cards quickly? Do sounds cut off? Layer?",[848,1439,1440,1443],{},[805,1441,1442],{},"Memory management"," - 100+ audio files across categories. Load them all? Stream them? Preload the current level?",[743,1445,1446],{},"We went through several iterations:",[1448,1449,1454],"pre",{"className":1450,"code":1452,"language":1453},[1451],"language-text","Version 1: Load all audio upfront\n→ Problem: 3-second app startup, memory issues\n\nVersion 2: Stream audio on demand\n→ Problem: Noticeable delay on first play\n\nVersion 3 (Final): Preload current category, lazy-load others\n→ Sweet spot of performance and responsiveness\n","text",[1455,1456,1452],"code",{"__ignoreMap":887},[1292,1458],{},[750,1460,1462],{"id":1461},"the-content-pipeline-generating-300-audio-assets","The Content Pipeline: Generating 300+ Audio Assets",[743,1464,1465],{},"Here's where it gets nerdy (in the best way).",[743,1467,1468,1469,1472],{},"When we started MemoSonic, we had a problem: we needed ",[805,1470,1471],{},"hundreds of audio files",". 168 chord sounds. 14 scale recordings. 8 rhythm patterns. Individual notes. Where do you even get that?",[743,1474,1475],{},[805,1476,1477],{},"Answer: You generate them programmatically.",[743,1479,1480],{},[1282,1481],{"alt":1482,"src":1483},"Terminal output","/images/blog/musictechlab_blog_how-we-built-memosonic_inline_2.png",[1325,1485,1487],{"id":1486},"synthesizing-piano-chords-with-fluidsynth","Synthesizing Piano Chords with FluidSynth",[743,1489,1490],{},"Instead of recording a pianist playing every chord (expensive, time-consuming), we wrote Python scripts that:",[1420,1492,1493,1499,1505],{},[848,1494,1495,1498],{},[805,1496,1497],{},"Generate MIDI files"," with the exact notes for each chord",[848,1500,1501,1504],{},[805,1502,1503],{},"Render them through FluidSynth"," using a high-quality Salamander Grand Piano soundfont",[848,1506,1507,1510],{},[805,1508,1509],{},"Convert to MP3"," via FFmpeg",[1448,1512,1516],{"className":1513,"code":1514,"language":1515,"meta":887,"style":887},"language-python shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","# Chord intervals (semitones from root)\nCHORD_TYPES = {\n    \"maj\": [0, 4, 7],           # Major: root, major 3rd, perfect 5th\n    \"min\": [0, 3, 7],           # Minor: root, minor 3rd, perfect 5th\n    \"7\": [0, 4, 7, 10],         # Dominant 7th\n    \"m7\": [0, 3, 7, 10],        # Minor 7th\n    \"maj7\": [0, 4, 7, 11],      # Major 7th\n    \"aug\": [0, 4, 8],           # Augmented\n    \"dim\": [0, 3, 6],           # Diminished\n}\n","python",[1455,1517,1518,1527,1540,1580,1609,1643,1676,1710,1740,1770],{"__ignoreMap":887},[1519,1520,1523],"span",{"class":1521,"line":1522},"line",1,[1519,1524,1526],{"class":1525},"sHwdD","# Chord intervals (semitones from root)\n",[1519,1528,1529,1533,1537],{"class":1521,"line":888},[1519,1530,1532],{"class":1531},"sTEyZ","CHORD_TYPES ",[1519,1534,1536],{"class":1535},"sMK4o","=",[1519,1538,1539],{"class":1535}," {\n",[1519,1541,1543,1546,1550,1553,1556,1559,1563,1566,1569,1571,1574,1577],{"class":1521,"line":1542},3,[1519,1544,1545],{"class":1535},"    \"",[1519,1547,1549],{"class":1548},"sfazB","maj",[1519,1551,1552],{"class":1535},"\"",[1519,1554,1555],{"class":1535},":",[1519,1557,1558],{"class":1535}," [",[1519,1560,1562],{"class":1561},"sbssI","0",[1519,1564,1565],{"class":1535},",",[1519,1567,1568],{"class":1561}," 4",[1519,1570,1565],{"class":1535},[1519,1572,1573],{"class":1561}," 7",[1519,1575,1576],{"class":1535},"],",[1519,1578,1579],{"class":1525},"           # Major: root, major 3rd, perfect 5th\n",[1519,1581,1582,1584,1587,1589,1591,1593,1595,1597,1600,1602,1604,1606],{"class":1521,"line":1241},[1519,1583,1545],{"class":1535},[1519,1585,1586],{"class":1548},"min",[1519,1588,1552],{"class":1535},[1519,1590,1555],{"class":1535},[1519,1592,1558],{"class":1535},[1519,1594,1562],{"class":1561},[1519,1596,1565],{"class":1535},[1519,1598,1599],{"class":1561}," 3",[1519,1601,1565],{"class":1535},[1519,1603,1573],{"class":1561},[1519,1605,1576],{"class":1535},[1519,1607,1608],{"class":1525},"           # Minor: root, minor 3rd, perfect 5th\n",[1519,1610,1612,1614,1617,1619,1621,1623,1625,1627,1629,1631,1633,1635,1638,1640],{"class":1521,"line":1611},5,[1519,1613,1545],{"class":1535},[1519,1615,1616],{"class":1548},"7",[1519,1618,1552],{"class":1535},[1519,1620,1555],{"class":1535},[1519,1622,1558],{"class":1535},[1519,1624,1562],{"class":1561},[1519,1626,1565],{"class":1535},[1519,1628,1568],{"class":1561},[1519,1630,1565],{"class":1535},[1519,1632,1573],{"class":1561},[1519,1634,1565],{"class":1535},[1519,1636,1637],{"class":1561}," 10",[1519,1639,1576],{"class":1535},[1519,1641,1642],{"class":1525},"         # Dominant 7th\n",[1519,1644,1646,1648,1651,1653,1655,1657,1659,1661,1663,1665,1667,1669,1671,1673],{"class":1521,"line":1645},6,[1519,1647,1545],{"class":1535},[1519,1649,1650],{"class":1548},"m7",[1519,1652,1552],{"class":1535},[1519,1654,1555],{"class":1535},[1519,1656,1558],{"class":1535},[1519,1658,1562],{"class":1561},[1519,1660,1565],{"class":1535},[1519,1662,1599],{"class":1561},[1519,1664,1565],{"class":1535},[1519,1666,1573],{"class":1561},[1519,1668,1565],{"class":1535},[1519,1670,1637],{"class":1561},[1519,1672,1576],{"class":1535},[1519,1674,1675],{"class":1525},"        # Minor 7th\n",[1519,1677,1679,1681,1684,1686,1688,1690,1692,1694,1696,1698,1700,1702,1705,1707],{"class":1521,"line":1678},7,[1519,1680,1545],{"class":1535},[1519,1682,1683],{"class":1548},"maj7",[1519,1685,1552],{"class":1535},[1519,1687,1555],{"class":1535},[1519,1689,1558],{"class":1535},[1519,1691,1562],{"class":1561},[1519,1693,1565],{"class":1535},[1519,1695,1568],{"class":1561},[1519,1697,1565],{"class":1535},[1519,1699,1573],{"class":1561},[1519,1701,1565],{"class":1535},[1519,1703,1704],{"class":1561}," 11",[1519,1706,1576],{"class":1535},[1519,1708,1709],{"class":1525},"      # Major 7th\n",[1519,1711,1713,1715,1718,1720,1722,1724,1726,1728,1730,1732,1735,1737],{"class":1521,"line":1712},8,[1519,1714,1545],{"class":1535},[1519,1716,1717],{"class":1548},"aug",[1519,1719,1552],{"class":1535},[1519,1721,1555],{"class":1535},[1519,1723,1558],{"class":1535},[1519,1725,1562],{"class":1561},[1519,1727,1565],{"class":1535},[1519,1729,1568],{"class":1561},[1519,1731,1565],{"class":1535},[1519,1733,1734],{"class":1561}," 8",[1519,1736,1576],{"class":1535},[1519,1738,1739],{"class":1525},"           # Augmented\n",[1519,1741,1743,1745,1748,1750,1752,1754,1756,1758,1760,1762,1765,1767],{"class":1521,"line":1742},9,[1519,1744,1545],{"class":1535},[1519,1746,1747],{"class":1548},"dim",[1519,1749,1552],{"class":1535},[1519,1751,1555],{"class":1535},[1519,1753,1558],{"class":1535},[1519,1755,1562],{"class":1561},[1519,1757,1565],{"class":1535},[1519,1759,1599],{"class":1561},[1519,1761,1565],{"class":1535},[1519,1763,1764],{"class":1561}," 6",[1519,1766,1576],{"class":1535},[1519,1768,1769],{"class":1525},"           # Diminished\n",[1519,1771,1773],{"class":1521,"line":1772},10,[1519,1774,1775],{"class":1535},"}\n",[743,1777,1778,1779,1782,1783,1415],{},"12 root notes × 7 chord types = ",[805,1780,1781],{},"84 chords",". Add enharmonic equivalents (C# = Db, etc.) and we hit ",[805,1784,1785],{},"168 unique chord files",[743,1787,1788],{},"The pipeline:",[1448,1790,1793],{"className":1791,"code":1792,"language":1453},[1451],"MIDI generation (midiutil)\n    ↓\nFluidSynth + Salamander Piano SF2\n    ↓\nWAV file\n    ↓\nFFmpeg MP3 encoding\n    ↓\nFinal audio asset\n",[1455,1794,1792],{"__ignoreMap":887},[1325,1796,1798],{"id":1797},"synthesizing-drum-patterns-from-scratch","Synthesizing Drum Patterns from Scratch",[743,1800,1801,1802,1415],{},"The rhythm category was even more interesting. We didn't use samples at all - we ",[805,1803,1804],{},"synthesized every drum sound mathematically",[1448,1806,1808],{"className":1513,"code":1807,"language":1515,"meta":887,"style":887},"def generate_kick(duration_ms=150):\n    \"\"\"Generate a punchy kick drum sound\"\"\"\n    for i in range(num_samples):\n        t = i / SAMPLE_RATE\n        # Pitch envelope: starts high, drops quickly\n        freq = freq_end + (freq_start - freq_end) * math.exp(-decay_rate * t)\n\n        # Main tone with pitch drop\n        tone = math.sin(2 * math.pi * freq * t)\n\n        # Add click transient at the start\n        if t \u003C 0.005:\n            click = (1 - t / 0.005) * 0.5\n        ...\n",[1455,1809,1810,1835,1847,1868,1883,1888,1942,1947,1952,1992,1996,2002,2020,2049],{"__ignoreMap":887},[1519,1811,1812,1816,1820,1823,1827,1829,1832],{"class":1521,"line":1522},[1519,1813,1815],{"class":1814},"spNyl","def",[1519,1817,1819],{"class":1818},"s2Zo4"," generate_kick",[1519,1821,1822],{"class":1535},"(",[1519,1824,1826],{"class":1825},"sHdIc","duration_ms",[1519,1828,1536],{"class":1535},[1519,1830,1831],{"class":1561},"150",[1519,1833,1834],{"class":1535},"):\n",[1519,1836,1837,1841,1844],{"class":1521,"line":888},[1519,1838,1840],{"class":1839},"s7zQu","    \"\"\"",[1519,1842,1843],{"class":1525},"Generate a punchy kick drum sound",[1519,1845,1846],{"class":1839},"\"\"\"\n",[1519,1848,1849,1852,1855,1858,1861,1863,1866],{"class":1521,"line":1542},[1519,1850,1851],{"class":1839},"    for",[1519,1853,1854],{"class":1531}," i ",[1519,1856,1857],{"class":1839},"in",[1519,1859,1860],{"class":1818}," range",[1519,1862,1822],{"class":1535},[1519,1864,1865],{"class":1818},"num_samples",[1519,1867,1834],{"class":1535},[1519,1869,1870,1873,1875,1877,1880],{"class":1521,"line":1241},[1519,1871,1872],{"class":1531},"        t ",[1519,1874,1536],{"class":1535},[1519,1876,1854],{"class":1531},[1519,1878,1879],{"class":1535},"/",[1519,1881,1882],{"class":1531}," SAMPLE_RATE\n",[1519,1884,1885],{"class":1521,"line":1611},[1519,1886,1887],{"class":1525},"        # Pitch envelope: starts high, drops quickly\n",[1519,1889,1890,1893,1895,1898,1901,1904,1907,1910,1913,1916,1919,1922,1924,1927,1930,1933,1936,1939],{"class":1521,"line":1645},[1519,1891,1892],{"class":1531},"        freq ",[1519,1894,1536],{"class":1535},[1519,1896,1897],{"class":1531}," freq_end ",[1519,1899,1900],{"class":1535},"+",[1519,1902,1903],{"class":1535}," (",[1519,1905,1906],{"class":1531},"freq_start ",[1519,1908,1909],{"class":1535},"-",[1519,1911,1912],{"class":1531}," freq_end",[1519,1914,1915],{"class":1535},")",[1519,1917,1918],{"class":1535}," *",[1519,1920,1921],{"class":1531}," math",[1519,1923,1415],{"class":1535},[1519,1925,1926],{"class":1818},"exp",[1519,1928,1929],{"class":1535},"(-",[1519,1931,1932],{"class":1818},"decay_rate ",[1519,1934,1935],{"class":1535},"*",[1519,1937,1938],{"class":1818}," t",[1519,1940,1941],{"class":1535},")\n",[1519,1943,1944],{"class":1521,"line":1678},[1519,1945,1946],{"emptyLinePlaceholder":906},"\n",[1519,1948,1949],{"class":1521,"line":1712},[1519,1950,1951],{"class":1525},"        # Main tone with pitch drop\n",[1519,1953,1954,1957,1959,1961,1963,1966,1968,1971,1973,1975,1977,1981,1983,1986,1988,1990],{"class":1521,"line":1742},[1519,1955,1956],{"class":1531},"        tone ",[1519,1958,1536],{"class":1535},[1519,1960,1921],{"class":1531},[1519,1962,1415],{"class":1535},[1519,1964,1965],{"class":1818},"sin",[1519,1967,1822],{"class":1535},[1519,1969,1970],{"class":1561},"2",[1519,1972,1918],{"class":1535},[1519,1974,1921],{"class":1818},[1519,1976,1415],{"class":1535},[1519,1978,1980],{"class":1979},"swJcz","pi",[1519,1982,1918],{"class":1535},[1519,1984,1985],{"class":1818}," freq ",[1519,1987,1935],{"class":1535},[1519,1989,1938],{"class":1818},[1519,1991,1941],{"class":1535},[1519,1993,1994],{"class":1521,"line":1772},[1519,1995,1946],{"emptyLinePlaceholder":906},[1519,1997,1999],{"class":1521,"line":1998},11,[1519,2000,2001],{"class":1525},"        # Add click transient at the start\n",[1519,2003,2005,2008,2011,2014,2017],{"class":1521,"line":2004},12,[1519,2006,2007],{"class":1839},"        if",[1519,2009,2010],{"class":1531}," t ",[1519,2012,2013],{"class":1535},"\u003C",[1519,2015,2016],{"class":1561}," 0.005",[1519,2018,2019],{"class":1535},":\n",[1519,2021,2023,2026,2028,2030,2033,2036,2038,2040,2042,2044,2046],{"class":1521,"line":2022},13,[1519,2024,2025],{"class":1531},"            click ",[1519,2027,1536],{"class":1535},[1519,2029,1903],{"class":1535},[1519,2031,2032],{"class":1561},"1",[1519,2034,2035],{"class":1535}," -",[1519,2037,2010],{"class":1531},[1519,2039,1879],{"class":1535},[1519,2041,2016],{"class":1561},[1519,2043,1915],{"class":1535},[1519,2045,1918],{"class":1535},[1519,2047,2048],{"class":1561}," 0.5\n",[1519,2050,2052],{"class":1521,"line":2051},14,[1519,2053,2054],{"class":1531},"        ...\n",[743,2056,2057,2060],{},[805,2058,2059],{},"Kick drum"," - A sine wave that rapidly drops in pitch (150Hz → 50Hz) with a click transient.",[743,2062,2063,2066],{},[805,2064,2065],{},"Snare drum"," - Shell resonance (170Hz with harmonics) + attack transient (450Hz) + filtered noise for the snare wires. Plus a touch of room reverb.",[743,2068,2069,2072],{},[805,2070,2071],{},"Hi-hat"," - White noise mixed with metallic high-frequency tones (6kHz, 8kHz, 10kHz), shaped with ADSR envelopes.",[743,2074,2075],{},"Then we programmed the actual rhythm patterns:",[1448,2077,2079],{"className":1513,"code":2078,"language":1515,"meta":887,"style":887},"RHYTHMS = [\n    (\"4_4\", \"4/4 Time\", \"4/4\", 100, [\n        (0, \"kick\", 0),      # Beat 1\n        (1, \"snare\", -2),    # Beat 2\n        (2, \"kick\", -2),     # Beat 3\n        (3, \"snare\", -2),    # Beat 4\n        # Off-beat hi-hats\n        (0.5, \"hihat\", -6),\n        (1.5, \"hihat\", -6),\n        ...\n    ]),\n    (\"swing\", \"Swing\", \"4/4\", 120, [\n        # Triplet-based timing for swing feel\n        (0.66, \"hihat\", -6),  # Swung eighth\n        ...\n    ]),\n]\n",[1455,2080,2081,2091,2131,2158,2184,2209,2235,2240,2266,2289,2293,2298,2335,2340,2366,2371,2376],{"__ignoreMap":887},[1519,2082,2083,2086,2088],{"class":1521,"line":1522},[1519,2084,2085],{"class":1531},"RHYTHMS ",[1519,2087,1536],{"class":1535},[1519,2089,2090],{"class":1535}," [\n",[1519,2092,2093,2096,2098,2101,2103,2105,2108,2111,2113,2115,2117,2120,2122,2124,2127,2129],{"class":1521,"line":888},[1519,2094,2095],{"class":1535},"    (",[1519,2097,1552],{"class":1535},[1519,2099,2100],{"class":1548},"4_4",[1519,2102,1552],{"class":1535},[1519,2104,1565],{"class":1535},[1519,2106,2107],{"class":1535}," \"",[1519,2109,2110],{"class":1548},"4/4 Time",[1519,2112,1552],{"class":1535},[1519,2114,1565],{"class":1535},[1519,2116,2107],{"class":1535},[1519,2118,2119],{"class":1548},"4/4",[1519,2121,1552],{"class":1535},[1519,2123,1565],{"class":1535},[1519,2125,2126],{"class":1561}," 100",[1519,2128,1565],{"class":1535},[1519,2130,2090],{"class":1535},[1519,2132,2133,2136,2138,2140,2142,2145,2147,2149,2152,2155],{"class":1521,"line":1542},[1519,2134,2135],{"class":1535},"        (",[1519,2137,1562],{"class":1561},[1519,2139,1565],{"class":1535},[1519,2141,2107],{"class":1535},[1519,2143,2144],{"class":1548},"kick",[1519,2146,1552],{"class":1535},[1519,2148,1565],{"class":1535},[1519,2150,2151],{"class":1561}," 0",[1519,2153,2154],{"class":1535},"),",[1519,2156,2157],{"class":1525},"      # Beat 1\n",[1519,2159,2160,2162,2164,2166,2168,2171,2173,2175,2177,2179,2181],{"class":1521,"line":1241},[1519,2161,2135],{"class":1535},[1519,2163,2032],{"class":1561},[1519,2165,1565],{"class":1535},[1519,2167,2107],{"class":1535},[1519,2169,2170],{"class":1548},"snare",[1519,2172,1552],{"class":1535},[1519,2174,1565],{"class":1535},[1519,2176,2035],{"class":1535},[1519,2178,1970],{"class":1561},[1519,2180,2154],{"class":1535},[1519,2182,2183],{"class":1525},"    # Beat 2\n",[1519,2185,2186,2188,2190,2192,2194,2196,2198,2200,2202,2204,2206],{"class":1521,"line":1611},[1519,2187,2135],{"class":1535},[1519,2189,1970],{"class":1561},[1519,2191,1565],{"class":1535},[1519,2193,2107],{"class":1535},[1519,2195,2144],{"class":1548},[1519,2197,1552],{"class":1535},[1519,2199,1565],{"class":1535},[1519,2201,2035],{"class":1535},[1519,2203,1970],{"class":1561},[1519,2205,2154],{"class":1535},[1519,2207,2208],{"class":1525},"     # Beat 3\n",[1519,2210,2211,2213,2216,2218,2220,2222,2224,2226,2228,2230,2232],{"class":1521,"line":1645},[1519,2212,2135],{"class":1535},[1519,2214,2215],{"class":1561},"3",[1519,2217,1565],{"class":1535},[1519,2219,2107],{"class":1535},[1519,2221,2170],{"class":1548},[1519,2223,1552],{"class":1535},[1519,2225,1565],{"class":1535},[1519,2227,2035],{"class":1535},[1519,2229,1970],{"class":1561},[1519,2231,2154],{"class":1535},[1519,2233,2234],{"class":1525},"    # Beat 4\n",[1519,2236,2237],{"class":1521,"line":1678},[1519,2238,2239],{"class":1525},"        # Off-beat hi-hats\n",[1519,2241,2242,2244,2247,2249,2251,2254,2256,2258,2260,2263],{"class":1521,"line":1712},[1519,2243,2135],{"class":1535},[1519,2245,2246],{"class":1561},"0.5",[1519,2248,1565],{"class":1535},[1519,2250,2107],{"class":1535},[1519,2252,2253],{"class":1548},"hihat",[1519,2255,1552],{"class":1535},[1519,2257,1565],{"class":1535},[1519,2259,2035],{"class":1535},[1519,2261,2262],{"class":1561},"6",[1519,2264,2265],{"class":1535},"),\n",[1519,2267,2268,2270,2273,2275,2277,2279,2281,2283,2285,2287],{"class":1521,"line":1742},[1519,2269,2135],{"class":1535},[1519,2271,2272],{"class":1561},"1.5",[1519,2274,1565],{"class":1535},[1519,2276,2107],{"class":1535},[1519,2278,2253],{"class":1548},[1519,2280,1552],{"class":1535},[1519,2282,1565],{"class":1535},[1519,2284,2035],{"class":1535},[1519,2286,2262],{"class":1561},[1519,2288,2265],{"class":1535},[1519,2290,2291],{"class":1521,"line":1772},[1519,2292,2054],{"class":1531},[1519,2294,2295],{"class":1521,"line":1998},[1519,2296,2297],{"class":1535},"    ]),\n",[1519,2299,2300,2302,2304,2307,2309,2311,2313,2316,2318,2320,2322,2324,2326,2328,2331,2333],{"class":1521,"line":2004},[1519,2301,2095],{"class":1535},[1519,2303,1552],{"class":1535},[1519,2305,2306],{"class":1548},"swing",[1519,2308,1552],{"class":1535},[1519,2310,1565],{"class":1535},[1519,2312,2107],{"class":1535},[1519,2314,2315],{"class":1548},"Swing",[1519,2317,1552],{"class":1535},[1519,2319,1565],{"class":1535},[1519,2321,2107],{"class":1535},[1519,2323,2119],{"class":1548},[1519,2325,1552],{"class":1535},[1519,2327,1565],{"class":1535},[1519,2329,2330],{"class":1561}," 120",[1519,2332,1565],{"class":1535},[1519,2334,2090],{"class":1535},[1519,2336,2337],{"class":1521,"line":2022},[1519,2338,2339],{"class":1525},"        # Triplet-based timing for swing feel\n",[1519,2341,2342,2344,2347,2349,2351,2353,2355,2357,2359,2361,2363],{"class":1521,"line":2051},[1519,2343,2135],{"class":1535},[1519,2345,2346],{"class":1561},"0.66",[1519,2348,1565],{"class":1535},[1519,2350,2107],{"class":1535},[1519,2352,2253],{"class":1548},[1519,2354,1552],{"class":1535},[1519,2356,1565],{"class":1535},[1519,2358,2035],{"class":1535},[1519,2360,2262],{"class":1561},[1519,2362,2154],{"class":1535},[1519,2364,2365],{"class":1525},"  # Swung eighth\n",[1519,2367,2369],{"class":1521,"line":2368},15,[1519,2370,2054],{"class":1531},[1519,2372,2374],{"class":1521,"line":2373},16,[1519,2375,2297],{"class":1535},[1519,2377,2379],{"class":1521,"line":2378},17,[1519,2380,2381],{"class":1535},"]\n",[743,2383,2384],{},"8 rhythm patterns, each with its own BPM, time signature, and accent patterns.",[1325,2386,2388],{"id":2387},"pure-tones-for-note-training","Pure Tones for Note Training",[743,2390,2391,2392,1555],{},"For the Notes category, we used ",[805,2393,2394],{},"pydub's sine wave generator",[1448,2396,2398],{"className":1513,"code":2397,"language":1515,"meta":887,"style":887},"NOTES = [\n    (\"c\", \"C\", 261.63),  # C4 (Middle C)\n    (\"d\", \"D\", 293.66),  # D4\n    (\"e\", \"E\", 329.63),  # E4\n    ...\n    (\"c_octave\", \"C (Octave)\", 523.25),  # C5\n]\n\ndef generate_note_audio(frequency, duration_ms):\n    tone = Sine(frequency).to_audio_segment(duration=duration_ms)\n    tone = tone.fade_in(50).fade_out(200)  # Avoid clicks\n    return tone\n",[1455,2399,2400,2409,2439,2469,2499,2504,2534,2538,2542,2561,2592,2626],{"__ignoreMap":887},[1519,2401,2402,2405,2407],{"class":1521,"line":1522},[1519,2403,2404],{"class":1531},"NOTES ",[1519,2406,1536],{"class":1535},[1519,2408,2090],{"class":1535},[1519,2410,2411,2413,2415,2418,2420,2422,2424,2427,2429,2431,2434,2436],{"class":1521,"line":888},[1519,2412,2095],{"class":1535},[1519,2414,1552],{"class":1535},[1519,2416,2417],{"class":1548},"c",[1519,2419,1552],{"class":1535},[1519,2421,1565],{"class":1535},[1519,2423,2107],{"class":1535},[1519,2425,2426],{"class":1548},"C",[1519,2428,1552],{"class":1535},[1519,2430,1565],{"class":1535},[1519,2432,2433],{"class":1561}," 261.63",[1519,2435,2154],{"class":1535},[1519,2437,2438],{"class":1525},"  # C4 (Middle C)\n",[1519,2440,2441,2443,2445,2448,2450,2452,2454,2457,2459,2461,2464,2466],{"class":1521,"line":1542},[1519,2442,2095],{"class":1535},[1519,2444,1552],{"class":1535},[1519,2446,2447],{"class":1548},"d",[1519,2449,1552],{"class":1535},[1519,2451,1565],{"class":1535},[1519,2453,2107],{"class":1535},[1519,2455,2456],{"class":1548},"D",[1519,2458,1552],{"class":1535},[1519,2460,1565],{"class":1535},[1519,2462,2463],{"class":1561}," 293.66",[1519,2465,2154],{"class":1535},[1519,2467,2468],{"class":1525},"  # D4\n",[1519,2470,2471,2473,2475,2478,2480,2482,2484,2487,2489,2491,2494,2496],{"class":1521,"line":1241},[1519,2472,2095],{"class":1535},[1519,2474,1552],{"class":1535},[1519,2476,2477],{"class":1548},"e",[1519,2479,1552],{"class":1535},[1519,2481,1565],{"class":1535},[1519,2483,2107],{"class":1535},[1519,2485,2486],{"class":1548},"E",[1519,2488,1552],{"class":1535},[1519,2490,1565],{"class":1535},[1519,2492,2493],{"class":1561}," 329.63",[1519,2495,2154],{"class":1535},[1519,2497,2498],{"class":1525},"  # E4\n",[1519,2500,2501],{"class":1521,"line":1611},[1519,2502,2503],{"class":1531},"    ...\n",[1519,2505,2506,2508,2510,2513,2515,2517,2519,2522,2524,2526,2529,2531],{"class":1521,"line":1645},[1519,2507,2095],{"class":1535},[1519,2509,1552],{"class":1535},[1519,2511,2512],{"class":1548},"c_octave",[1519,2514,1552],{"class":1535},[1519,2516,1565],{"class":1535},[1519,2518,2107],{"class":1535},[1519,2520,2521],{"class":1548},"C (Octave)",[1519,2523,1552],{"class":1535},[1519,2525,1565],{"class":1535},[1519,2527,2528],{"class":1561}," 523.25",[1519,2530,2154],{"class":1535},[1519,2532,2533],{"class":1525},"  # C5\n",[1519,2535,2536],{"class":1521,"line":1678},[1519,2537,2381],{"class":1535},[1519,2539,2540],{"class":1521,"line":1712},[1519,2541,1946],{"emptyLinePlaceholder":906},[1519,2543,2544,2546,2549,2551,2554,2556,2559],{"class":1521,"line":1742},[1519,2545,1815],{"class":1814},[1519,2547,2548],{"class":1818}," generate_note_audio",[1519,2550,1822],{"class":1535},[1519,2552,2553],{"class":1825},"frequency",[1519,2555,1565],{"class":1535},[1519,2557,2558],{"class":1825}," duration_ms",[1519,2560,1834],{"class":1535},[1519,2562,2563,2566,2568,2571,2573,2575,2578,2581,2583,2586,2588,2590],{"class":1521,"line":1772},[1519,2564,2565],{"class":1531},"    tone ",[1519,2567,1536],{"class":1535},[1519,2569,2570],{"class":1818}," Sine",[1519,2572,1822],{"class":1535},[1519,2574,2553],{"class":1818},[1519,2576,2577],{"class":1535},").",[1519,2579,2580],{"class":1818},"to_audio_segment",[1519,2582,1822],{"class":1535},[1519,2584,2585],{"class":1825},"duration",[1519,2587,1536],{"class":1535},[1519,2589,1826],{"class":1818},[1519,2591,1941],{"class":1535},[1519,2593,2594,2596,2598,2601,2603,2606,2608,2611,2613,2616,2618,2621,2623],{"class":1521,"line":1998},[1519,2595,2565],{"class":1531},[1519,2597,1536],{"class":1535},[1519,2599,2600],{"class":1531}," tone",[1519,2602,1415],{"class":1535},[1519,2604,2605],{"class":1818},"fade_in",[1519,2607,1822],{"class":1535},[1519,2609,2610],{"class":1561},"50",[1519,2612,2577],{"class":1535},[1519,2614,2615],{"class":1818},"fade_out",[1519,2617,1822],{"class":1535},[1519,2619,2620],{"class":1561},"200",[1519,2622,1915],{"class":1535},[1519,2624,2625],{"class":1525},"  # Avoid clicks\n",[1519,2627,2628,2631],{"class":1521,"line":2004},[1519,2629,2630],{"class":1839},"    return",[1519,2632,2633],{"class":1531}," tone\n",[743,2635,2636],{},"Simple, clean, and perfect for ear training.",[1325,2638,2640],{"id":2639},"post-processing-cutting-trimming-normalizing","Post-Processing: Cutting, Trimming, Normalizing",[743,2642,2643],{},"Raw generated audio isn't always game-ready. We wrote additional scripts:",[743,2645,2646,2649],{},[805,2647,2648],{},"Cut scales to ascending only"," - Original scales went up AND down. Too long. We cut them to just the ascending portion:",[1448,2651,2653],{"className":1513,"code":2652,"language":1515,"meta":887,"style":887},"def cut_to_ascending(file_path):\n    audio = AudioSegment.from_mp3(file_path)\n    half_point = len(audio) // 2\n    ascending_only = audio[:half_point]\n    ascending_only.export(file_path, format=\"mp3\")\n",[1455,2654,2655,2669,2690,2713,2731],{"__ignoreMap":887},[1519,2656,2657,2659,2662,2664,2667],{"class":1521,"line":1522},[1519,2658,1815],{"class":1814},[1519,2660,2661],{"class":1818}," cut_to_ascending",[1519,2663,1822],{"class":1535},[1519,2665,2666],{"class":1825},"file_path",[1519,2668,1834],{"class":1535},[1519,2670,2671,2674,2676,2679,2681,2684,2686,2688],{"class":1521,"line":888},[1519,2672,2673],{"class":1531},"    audio ",[1519,2675,1536],{"class":1535},[1519,2677,2678],{"class":1531}," AudioSegment",[1519,2680,1415],{"class":1535},[1519,2682,2683],{"class":1818},"from_mp3",[1519,2685,1822],{"class":1535},[1519,2687,2666],{"class":1818},[1519,2689,1941],{"class":1535},[1519,2691,2692,2695,2697,2700,2702,2705,2707,2710],{"class":1521,"line":1542},[1519,2693,2694],{"class":1531},"    half_point ",[1519,2696,1536],{"class":1535},[1519,2698,2699],{"class":1818}," len",[1519,2701,1822],{"class":1535},[1519,2703,2704],{"class":1818},"audio",[1519,2706,1915],{"class":1535},[1519,2708,2709],{"class":1535}," //",[1519,2711,2712],{"class":1561}," 2\n",[1519,2714,2715,2718,2720,2723,2726,2729],{"class":1521,"line":1241},[1519,2716,2717],{"class":1531},"    ascending_only ",[1519,2719,1536],{"class":1535},[1519,2721,2722],{"class":1531}," audio",[1519,2724,2725],{"class":1535},"[:",[1519,2727,2728],{"class":1531},"half_point",[1519,2730,2381],{"class":1535},[1519,2732,2733,2736,2738,2741,2743,2745,2747,2750,2752,2754,2757,2759],{"class":1521,"line":1611},[1519,2734,2735],{"class":1531},"    ascending_only",[1519,2737,1415],{"class":1535},[1519,2739,2740],{"class":1818},"export",[1519,2742,1822],{"class":1535},[1519,2744,2666],{"class":1818},[1519,2746,1565],{"class":1535},[1519,2748,2749],{"class":1825}," format",[1519,2751,1536],{"class":1535},[1519,2753,1552],{"class":1535},[1519,2755,2756],{"class":1548},"mp3",[1519,2758,1552],{"class":1535},[1519,2760,1941],{"class":1535},[743,2762,2763,2766],{},[805,2764,2765],{},"Trim chord arpeggios"," - Some chords had unwanted arpeggio intros. FFmpeg to the rescue:",[1448,2768,2772],{"className":2769,"code":2770,"language":2771,"meta":887,"style":887},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","ffmpeg -y -i chord.mp3 -ss [start_time] -acodec libmp3lame chord_trimmed.mp3\n","bash",[1455,2773,2774],{"__ignoreMap":887},[1519,2775,2776,2780,2783,2786,2789,2792],{"class":1521,"line":1522},[1519,2777,2779],{"class":2778},"sBMFI","ffmpeg",[1519,2781,2782],{"class":1548}," -y",[1519,2784,2785],{"class":1548}," -i",[1519,2787,2788],{"class":1548}," chord.mp3",[1519,2790,2791],{"class":1548}," -ss",[1519,2793,2794],{"class":1531}," [start_time] -acodec libmp3lame chord_trimmed.mp3\n",[743,2796,2797,2800],{},[805,2798,2799],{},"Batch update SVG colors"," - Our chord diagrams needed color adjustments to match the app theme. Regex-based batch processing:",[1448,2802,2804],{"className":1513,"code":2803,"language":1515,"meta":887,"style":887},"COLOR_REPLACEMENTS = {\n    '#f3f8f3': '#000000',  # Inactive keys: light → dark\n    '#b3cc57': '#C6F222',  # Active keys: old green → MTL lime\n}\n",[1455,2805,2806,2815,2841,2864],{"__ignoreMap":887},[1519,2807,2808,2811,2813],{"class":1521,"line":1522},[1519,2809,2810],{"class":1531},"COLOR_REPLACEMENTS ",[1519,2812,1536],{"class":1535},[1519,2814,1539],{"class":1535},[1519,2816,2817,2820,2823,2826,2828,2831,2834,2836,2838],{"class":1521,"line":888},[1519,2818,2819],{"class":1535},"    '",[1519,2821,2822],{"class":1548},"#f3f8f3",[1519,2824,2825],{"class":1535},"'",[1519,2827,1555],{"class":1535},[1519,2829,2830],{"class":1535}," '",[1519,2832,2833],{"class":1548},"#000000",[1519,2835,2825],{"class":1535},[1519,2837,1565],{"class":1535},[1519,2839,2840],{"class":1525},"  # Inactive keys: light → dark\n",[1519,2842,2843,2845,2848,2850,2852,2854,2857,2859,2861],{"class":1521,"line":1542},[1519,2844,2819],{"class":1535},[1519,2846,2847],{"class":1548},"#b3cc57",[1519,2849,2825],{"class":1535},[1519,2851,1555],{"class":1535},[1519,2853,2830],{"class":1535},[1519,2855,2856],{"class":1548},"#C6F222",[1519,2858,2825],{"class":1535},[1519,2860,1565],{"class":1535},[1519,2862,2863],{"class":1525},"  # Active keys: old green → MTL lime\n",[1519,2865,2866],{"class":1521,"line":1241},[1519,2867,1775],{"class":1535},[1325,2869,2871],{"id":2870},"the-numbers","The Numbers",[743,2873,2874],{},"By the end, our content pipeline generated:",[1151,2876,2877,2890],{},[1154,2878,2879],{},[1157,2880,2881,2884,2887],{},[1160,2882,2883],{},"Category",[1160,2885,2886],{},"Files",[1160,2888,2889],{},"Method",[1165,2891,2892,2903,2913,2924,2935,2946],{},[1157,2893,2894,2897,2900],{},[1170,2895,2896],{},"Chords",[1170,2898,2899],{},"168 audio + 168 SVG",[1170,2901,2902],{},"MIDI → FluidSynth → FFmpeg",[1157,2904,2905,2908,2911],{},[1170,2906,2907],{},"Scales",[1170,2909,2910],{},"14 audio + 14 SVG",[1170,2912,2902],{},[1157,2914,2915,2918,2921],{},[1170,2916,2917],{},"Notes",[1170,2919,2920],{},"8 audio + 8 SVG",[1170,2922,2923],{},"pydub sine wave synthesis",[1157,2925,2926,2929,2932],{},[1170,2927,2928],{},"Rhythms",[1170,2930,2931],{},"8 audio + 8 PNG",[1170,2933,2934],{},"Mathematical drum synthesis",[1157,2936,2937,2940,2943],{},[1170,2938,2939],{},"Animals",[1170,2941,2942],{},"18 audio + 18 PNG",[1170,2944,2945],{},"Curated library",[1157,2947,2948,2951,2953],{},[1170,2949,2950],{},"Instruments",[1170,2952,2931],{},[1170,2954,2955],{},"Curated samples",[743,2957,2958,2961],{},[805,2959,2960],{},"Total: 300+ assets",", mostly generated programmatically.",[1325,2963,2965],{"id":2964},"why-this-matters","Why This Matters",[743,2967,2968],{},"Could we have licensed a chord library? Sure. But:",[1420,2970,2971,2977,2983,2989],{},[848,2972,2973,2976],{},[805,2974,2975],{},"Consistency"," - Every chord sounds identical in timbre, velocity, duration",[848,2978,2979,2982],{},[805,2980,2981],{},"Customization"," - Need a longer sustain? Change one variable, regenerate",[848,2984,2985,2988],{},[805,2986,2987],{},"No licensing headaches"," - We own every bit of audio",[848,2990,2991,2994],{},[805,2992,2993],{},"Educational value"," - We actually understand what we're teaching",[743,2996,2997,2998,1415],{},"Plus, writing a drum synthesizer from scratch is just ",[1263,2999,3000],{},"fun",[1292,3002],{},[750,3004,3006],{"id":3005},"the-categories-more-than-just-music","The Categories: More Than Just Music",[743,3008,3009],{},"While we started with music education in mind, we quickly realized the concept works for much more.",[1325,3011,3013],{"id":3012},"musical-categories","Musical Categories",[755,3015,1276,3019,3024,3025],{"className":3016},[1272,1273,3017,3018],"gap-8","items-start",[1282,3020],{"src":3021,"alt":3022,"className":3023},"/images/blog/musictechlab_blog_how-we-built-memosonic_inline_3.webp","Category selection",[1287],"\n   ",[1282,3026],{"src":3027,"alt":3028,"className":3029},"/images/blog/musictechlab_blog_how-we-built-memosonic_inline_4.webp","Chord diagram",[1287],[743,3031,3032,3034],{},[805,3033,2896],{}," - Can you tell the difference between a major and minor chord? What about diminished vs. augmented? This category trains your ear to recognize the emotional quality of different chord types.",[743,3036,3037,3039],{},[805,3038,2907],{}," - From the bright C Major to the melancholic A Natural Minor, players learn to identify scales by their unique character.",[743,3041,3042,3044],{},[805,3043,2917],{}," - Perfect for beginners learning to identify individual pitches on the musical staff.",[743,3046,3047,3049],{},[805,3048,2928],{}," - 3/4 waltz? 4/4 rock beat? Syncopated funk? Train your rhythmic ear.",[1325,3051,3053],{"id":3052},"beyond-music","Beyond Music",[743,3055,3056,3058],{},[805,3057,2939],{}," - 18 animal sounds, from the roar of a lion to the chirp of a bird. Perfect for younger kids or anyone who just wants a fun challenge.",[743,3060,3061,3063],{},[805,3062,2950],{}," - Can you distinguish a trumpet from a saxophone? A violin from a cello? Harder than you'd think!",[1292,3065],{},[750,3067,3069],{"id":3068},"the-game-modes-flexibility-matters","The Game Modes: Flexibility Matters",[743,3071,3072],{},"Not everyone learns the same way. That's why MemoSonic offers two distinct game modes:",[1325,3074,3076],{"id":3075},"memosonic-mode-sound-first","Memosonic Mode (Sound-First)",[743,3078,3079],{},"This is the heart of the app. Tap a card, hear a sound. Remember that sound. Find its match.",[1448,3081,3084],{"className":3082,"code":3083,"language":1453},[1451],"1. Tap card → Hear sound (no visual)\n2. Tap another card → Hear second sound\n3. Match? → Cards reveal and stay\n4. No match? → Cards flip back, remember the sounds!\n",[1455,3085,3083],{"__ignoreMap":887},[755,3087,1276,3089,1276,3092,1276,3097],{"className":3088},[1272,1273,1274,1275],[755,3090],{"className":3091},[1280],[1282,3093],{"src":3094,"alt":3095,"className":3096},"/images/blog/musictechlab_blog_how-we-built-memosonic_inline_5.webp","Memosonic mode",[1287],[755,3098],{"className":3099},[1280],[1325,3101,3103],{"id":3102},"memo-classic-mode-visual-first","Memo Classic Mode (Visual-First)",[743,3105,3106],{},"For those who want a traditional experience, or as a comparison to understand how much harder audio matching is!",[1448,3108,3111],{"className":3109,"code":3110,"language":1453},[1451],"1. Tap card → Image briefly appears (1 second)\n2. Tap another card → Second image appears\n3. Match? → Cards stay revealed\n4. No match? → Images hide, remember positions!\n",[1455,3112,3110],{"__ignoreMap":887},[1292,3114],{},[750,3116,3118],{"id":3117},"difficulty-levels-progressive-challenge","Difficulty Levels: Progressive Challenge",[743,3120,3121],{},"We designed three difficulty levels, each carefully balanced:",[1151,3123,3124,3140],{},[1154,3125,3126],{},[1157,3127,3128,3131,3134,3137],{},[1160,3129,3130],{},"Level",[1160,3132,3133],{},"Cards",[1160,3135,3136],{},"Pairs",[1160,3138,3139],{},"Estimated Time",[1165,3141,3142,3154,3167],{},[1157,3143,3144,3147,3149,3151],{},[1170,3145,3146],{},"Easy",[1170,3148,2262],{},[1170,3150,2215],{},[1170,3152,3153],{},"2-3 minutes",[1157,3155,3156,3159,3162,3164],{},[1170,3157,3158],{},"Normal",[1170,3160,3161],{},"12",[1170,3163,2262],{},[1170,3165,3166],{},"5-7 minutes",[1157,3168,3169,3172,3175,3178],{},[1170,3170,3171],{},"Hard",[1170,3173,3174],{},"20",[1170,3176,3177],{},"10",[1170,3179,3180],{},"10-15 minutes",[755,3182,1276,3184,1276,3187,1276,3192],{"className":3183},[1272,1273,1274,1275],[755,3185],{"className":3186},[1280],[1282,3188],{"src":3189,"alt":3190,"className":3191},"/images/blog/musictechlab_blog_how-we-built-memosonic_inline_7.webp","Level selection",[1287],[755,3193],{"className":3194},[1280],[743,3196,3197],{},"The jump from 6 to 12 cards isn't just \"twice as hard\" - it's exponentially more challenging. Your brain can hold about 7 items in working memory. At 12 cards, you're constantly pushing that limit.",[743,3199,3200],{},"At 20 cards? It's a real workout.",[1292,3202],{},[750,3204,3206],{"id":3205},"accessibility-designing-for-everyone","Accessibility: Designing for Everyone",[743,3208,3209],{},"Here's where it gets important.",[1325,3211,3213],{"id":3212},"the-visually-impaired-perspective","The Visually Impaired Perspective",[743,3215,3216],{},"Traditional memory games are impossible for blind or visually impaired players. The entire mechanic relies on seeing and remembering visual positions.",[743,3218,3219],{},[805,3220,3221],{},"MemoSonic flips this on its head.",[743,3223,3224],{},"In Memosonic mode, vision is secondary. You're listening, remembering sounds, matching audio. A visually impaired player can:",[1420,3226,3227,3230,3233,3236],{},[848,3228,3229],{},"Navigate the grid using screen reader or spatial memory",[848,3231,3232],{},"Tap cards to hear sounds",[848,3234,3235],{},"Match based purely on audio memory",[848,3237,3238],{},"Receive audio feedback on success/failure",[755,3240,1276,3242,1276,3245,1276,3250],{"className":3241},[1272,1273,1274,1275],[755,3243],{"className":3244},[1280],[1282,3246],{"src":3247,"alt":3248,"className":3249},"/images/blog/musictechlab_blog_how-we-built-memosonic_inline_8.webp","Game completion",[1287],[755,3251],{"className":3252},[1280],[1325,3254,3256],{"id":3255},"design-decisions-for-accessibility","Design Decisions for Accessibility",[743,3258,3259],{},"We made several deliberate choices:",[743,3261,3262],{},[805,3263,3264],{},"High Contrast UI",[845,3266,3267,3270,3273,3276],{},[848,3268,3269],{},"Dark background (#0E0F11)",[848,3271,3272],{},"Bright lime yellow accents (#C6F222)",[848,3274,3275],{},"Clear white text",[848,3277,3278],{},"No reliance on color alone for meaning",[743,3280,3281],{},[805,3282,3283],{},"Large Touch Targets",[845,3285,3286,3289,3292],{},[848,3287,3288],{},"Cards are generously sized",[848,3290,3291],{},"Buttons have ample padding",[848,3293,3294],{},"No precision tapping required",[743,3296,3297],{},[805,3298,3299],{},"Audio Feedback",[845,3301,3302,3305,3308],{},[848,3303,3304],{},"Every interaction has sound",[848,3306,3307],{},"Success/failure clearly distinguishable",[848,3309,3310],{},"No silent failures",[743,3312,3313],{},[805,3314,3315],{},"Simple Navigation",[845,3317,3318,3321,3324],{},[848,3319,3320],{},"Linear flow: Home → Category → Level → Game",[848,3322,3323],{},"Back button always available",[848,3325,3326],{},"No complex gestures required",[1292,3328],{},[750,3330,3332],{"id":3331},"the-technical-deep-dive-for-fellow-developers","The Technical Deep Dive (For Fellow Developers)",[1325,3334,3336],{"id":3335},"project-architecture","Project Architecture",[1448,3338,3341],{"className":3339,"code":3340,"language":1453},[1451],"lib/\n├── main.dart                    # Entry point, splash screen\n├── core/\n│   ├── routes.dart              # GoRouter navigation\n│   ├── theme.dart               # Material 3 dark theme\n│   └── game_utils.dart          # Category definitions, game data\n├── features/\n│   ├── home_screen.dart         # Category selection grid\n│   ├── level_screen.dart        # Difficulty picker\n│   ├── game_screen.dart         # Main gameplay\n│   └── settings_screen.dart     # Settings hub\n└── widgets/\n    └── app_logo.dart            # Reusable logo component\n",[1455,3342,3340],{"__ignoreMap":887},[1325,3344,3346],{"id":3345},"key-dependencies","Key Dependencies",[1448,3348,3352],{"className":3349,"code":3350,"language":3351,"meta":887,"style":887},"language-yaml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","dependencies:\n  flutter_riverpod: ^3.0.1    # State management\n  go_router: ^16.3.0          # Navigation\n  just_audio: ^0.10.5         # Primary audio engine\n  audioplayers: ^6.1.0        # Alternative audio playback\n  flutter_svg: ^2.0.10        # SVG rendering for diagrams\n  shared_preferences: ^2.5.3  # Local settings storage\n","yaml",[1455,3353,3354,3361,3374,3387,3400,3413,3426],{"__ignoreMap":887},[1519,3355,3356,3359],{"class":1521,"line":1522},[1519,3357,3358],{"class":1979},"dependencies",[1519,3360,2019],{"class":1535},[1519,3362,3363,3366,3368,3371],{"class":1521,"line":888},[1519,3364,3365],{"class":1979},"  flutter_riverpod",[1519,3367,1555],{"class":1535},[1519,3369,3370],{"class":1548}," ^3.0.1",[1519,3372,3373],{"class":1525},"    # State management\n",[1519,3375,3376,3379,3381,3384],{"class":1521,"line":1542},[1519,3377,3378],{"class":1979},"  go_router",[1519,3380,1555],{"class":1535},[1519,3382,3383],{"class":1548}," ^16.3.0",[1519,3385,3386],{"class":1525},"          # Navigation\n",[1519,3388,3389,3392,3394,3397],{"class":1521,"line":1241},[1519,3390,3391],{"class":1979},"  just_audio",[1519,3393,1555],{"class":1535},[1519,3395,3396],{"class":1548}," ^0.10.5",[1519,3398,3399],{"class":1525},"         # Primary audio engine\n",[1519,3401,3402,3405,3407,3410],{"class":1521,"line":1611},[1519,3403,3404],{"class":1979},"  audioplayers",[1519,3406,1555],{"class":1535},[1519,3408,3409],{"class":1548}," ^6.1.0",[1519,3411,3412],{"class":1525},"        # Alternative audio playback\n",[1519,3414,3415,3418,3420,3423],{"class":1521,"line":1645},[1519,3416,3417],{"class":1979},"  flutter_svg",[1519,3419,1555],{"class":1535},[1519,3421,3422],{"class":1548}," ^2.0.10",[1519,3424,3425],{"class":1525},"        # SVG rendering for diagrams\n",[1519,3427,3428,3431,3433,3436],{"class":1521,"line":1678},[1519,3429,3430],{"class":1979},"  shared_preferences",[1519,3432,1555],{"class":1535},[1519,3434,3435],{"class":1548}," ^2.5.3",[1519,3437,3438],{"class":1525},"  # Local settings storage\n",[1325,3440,3442],{"id":3441},"the-card-flip-animation","The Card Flip Animation",[743,3444,3445],{},"One of the most satisfying parts of the app is the card flip animation. Here's the approach:",[1448,3447,3451],{"className":3448,"code":3449,"language":3450,"meta":887,"style":887},"language-dart shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","// Simplified card flip logic\nAnimatedBuilder(\n  animation: _flipAnimation,\n  builder: (context, child) {\n    final angle = _flipAnimation.value * pi;\n    final isFront = angle \u003C pi / 2;\n\n    return Transform(\n      transform: Matrix4.identity()\n        ..setEntry(3, 2, 0.001)  // Perspective\n        ..rotateY(angle),\n      alignment: Alignment.center,\n      child: isFront ? _buildFrontFace() : _buildBackFace(),\n    );\n  },\n)\n","dart",[1455,3452,3453,3458,3466,3479,3494,3519,3542,3546,3555,3573,3600,3612,3629,3657,3664,3671],{"__ignoreMap":887},[1519,3454,3455],{"class":1521,"line":1522},[1519,3456,3457],{"class":1525},"// Simplified card flip logic\n",[1519,3459,3460,3463],{"class":1521,"line":888},[1519,3461,3462],{"class":2778},"AnimatedBuilder",[1519,3464,3465],{"class":1531},"(\n",[1519,3467,3468,3471,3473,3476],{"class":1521,"line":1542},[1519,3469,3470],{"class":1531},"  animation",[1519,3472,1555],{"class":1535},[1519,3474,3475],{"class":1531}," _flipAnimation",[1519,3477,3478],{"class":1535},",\n",[1519,3480,3481,3484,3486,3489,3491],{"class":1521,"line":1241},[1519,3482,3483],{"class":1531},"  builder",[1519,3485,1555],{"class":1535},[1519,3487,3488],{"class":1531}," (context",[1519,3490,1565],{"class":1535},[1519,3492,3493],{"class":1531}," child) {\n",[1519,3495,3496,3499,3502,3504,3506,3508,3511,3513,3516],{"class":1521,"line":1611},[1519,3497,3498],{"class":1814},"    final",[1519,3500,3501],{"class":1531}," angle ",[1519,3503,1536],{"class":1535},[1519,3505,3475],{"class":1531},[1519,3507,1415],{"class":1535},[1519,3509,3510],{"class":1531},"value ",[1519,3512,1935],{"class":1535},[1519,3514,3515],{"class":1531}," pi",[1519,3517,3518],{"class":1535},";\n",[1519,3520,3521,3523,3526,3528,3530,3532,3535,3537,3540],{"class":1521,"line":1645},[1519,3522,3498],{"class":1814},[1519,3524,3525],{"class":1531}," isFront ",[1519,3527,1536],{"class":1535},[1519,3529,3501],{"class":1531},[1519,3531,2013],{"class":1535},[1519,3533,3534],{"class":1531}," pi ",[1519,3536,1879],{"class":1535},[1519,3538,3539],{"class":1561}," 2",[1519,3541,3518],{"class":1535},[1519,3543,3544],{"class":1521,"line":1678},[1519,3545,1946],{"emptyLinePlaceholder":906},[1519,3547,3548,3550,3553],{"class":1521,"line":1712},[1519,3549,2630],{"class":1839},[1519,3551,3552],{"class":2778}," Transform",[1519,3554,3465],{"class":1531},[1519,3556,3557,3560,3562,3565,3567,3570],{"class":1521,"line":1742},[1519,3558,3559],{"class":1531},"      transform",[1519,3561,1555],{"class":1535},[1519,3563,3564],{"class":2778}," Matrix4",[1519,3566,1415],{"class":1535},[1519,3568,3569],{"class":1818},"identity",[1519,3571,3572],{"class":1531},"()\n",[1519,3574,3575,3578,3581,3583,3585,3587,3589,3591,3594,3597],{"class":1521,"line":1772},[1519,3576,3577],{"class":1535},"        ..",[1519,3579,3580],{"class":1818},"setEntry",[1519,3582,1822],{"class":1531},[1519,3584,2215],{"class":1561},[1519,3586,1565],{"class":1535},[1519,3588,3539],{"class":1561},[1519,3590,1565],{"class":1535},[1519,3592,3593],{"class":1561}," 0.001",[1519,3595,3596],{"class":1531},")  ",[1519,3598,3599],{"class":1525},"// Perspective\n",[1519,3601,3602,3604,3607,3610],{"class":1521,"line":1998},[1519,3603,3577],{"class":1535},[1519,3605,3606],{"class":1818},"rotateY",[1519,3608,3609],{"class":1531},"(angle)",[1519,3611,3478],{"class":1535},[1519,3613,3614,3617,3619,3622,3624,3627],{"class":1521,"line":2004},[1519,3615,3616],{"class":1531},"      alignment",[1519,3618,1555],{"class":1535},[1519,3620,3621],{"class":2778}," Alignment",[1519,3623,1415],{"class":1535},[1519,3625,3626],{"class":1531},"center",[1519,3628,3478],{"class":1535},[1519,3630,3631,3634,3636,3638,3641,3644,3647,3649,3652,3655],{"class":1521,"line":2022},[1519,3632,3633],{"class":1531},"      child",[1519,3635,1555],{"class":1535},[1519,3637,3525],{"class":1531},[1519,3639,3640],{"class":1535},"?",[1519,3642,3643],{"class":1818}," _buildFrontFace",[1519,3645,3646],{"class":1531},"() ",[1519,3648,1555],{"class":1535},[1519,3650,3651],{"class":1818}," _buildBackFace",[1519,3653,3654],{"class":1531},"()",[1519,3656,3478],{"class":1535},[1519,3658,3659,3662],{"class":1521,"line":2051},[1519,3660,3661],{"class":1531},"    )",[1519,3663,3518],{"class":1535},[1519,3665,3666,3669],{"class":1521,"line":2368},[1519,3667,3668],{"class":1531},"  }",[1519,3670,3478],{"class":1535},[1519,3672,3673],{"class":1521,"line":2373},[1519,3674,1941],{"class":1531},[743,3676,3677,3678,3681],{},"The trick is the perspective transform (",[1455,3679,3680],{},"setEntry(3, 2, 0.001)",") - it gives that satisfying 3D effect without being distracting.",[1292,3683],{},[750,3685,3687],{"id":3686},"lessons-learned","Lessons Learned",[1325,3689,3691],{"id":3690},"_1-audio-latency-is-everything","1. Audio Latency is Everything",[743,3693,3694],{},"In a sound-based game, even tiny delays feel wrong. We spent weeks optimizing audio playback to ensure instant response.",[1325,3696,3698],{"id":3697},"_2-kids-are-brutally-honest-testers","2. Kids Are Brutally Honest Testers",[743,3700,3701],{},"Our first playtest with actual children revealed:",[845,3703,3704,3707,3710],{},[848,3705,3706],{},"\"Why is this taking so long?\" (loading screen was 2 seconds)",[848,3708,3709],{},"\"I already heard that one!\" (audio caching issue)",[848,3711,3712],{},"\"This is too easy!\" (we added Hard mode)",[1325,3714,3716],{"id":3715},"_3-accessibility-isnt-an-afterthought","3. Accessibility Isn't an Afterthought",[743,3718,3719],{},"Building for accessibility from day one is 10x easier than retrofitting. The decisions we made early (audio-first gameplay, high contrast, large targets) paid off.",[1325,3721,3723],{"id":3722},"_4-simple-beats-complex","4. Simple Beats Complex",[743,3725,3726],{},"Our first design had:",[845,3728,3729,3732,3735,3738],{},[848,3730,3731],{},"User accounts",[848,3733,3734],{},"Leaderboards",[848,3736,3737],{},"Achievement systems",[848,3739,3740],{},"Daily challenges",[743,3742,3743],{},"We cut all of it. The core experience - match sounds, train your ear - didn't need any of that. It needed to work flawlessly.",[1292,3745],{},[750,3747,3749],{"id":3748},"whats-next-you-tell-us","What's Next? You Tell Us!",[743,3751,3752,3753,1415],{},"We have a bunch of ideas brewing for the next iteration. But here's the thing - ",[805,3754,3755],{},"we'd rather build what you actually want",[743,3757,3758],{},"Take a look at what we're considering:",[743,3760,3761],{},[805,3762,3763],{},"More Categories",[845,3765,3766,3769,3772],{},[848,3767,3768],{},"🐦 Bird songs (nature education)",[848,3770,3771],{},"🌍 World languages (basic vocabulary)",[848,3773,3774],{},"🎼 Famous melodies (classical music education)",[743,3776,3777],{},[805,3778,3779],{},"Enhanced Accessibility",[845,3781,3782,3785,3788],{},[848,3783,3784],{},"🔊 Full VoiceOver/TalkBack support",[848,3786,3787],{},"📳 Haptic feedback for matches",[848,3789,3790],{},"🎧 Audio descriptions for all UI elements",[743,3792,3793],{},[805,3794,3795],{},"Multiplayer Mode",[845,3797,3798,3801,3804],{},[848,3799,3800],{},"👥 Turn-based competition",[848,3802,3803],{},"⚡ Who can match faster?",[848,3805,3806],{},"🏠 Family game night feature",[743,3808,3809],{},[805,3810,3811],{},"Something else entirely?",[743,3813,3814],{},"Maybe you're a music teacher who needs specific intervals training. Maybe you work with visually impaired students and have insights we haven't considered. Maybe your kid is obsessed with dinosaurs and you want dinosaur sounds.",[743,3816,3817],{},[805,3818,3819],{},"We're listening.",[743,3821,3822,3823,3827],{},"Drop us a line at ",[827,3824,3826],{"href":3825},"mailto:support@musictechlab.io","support@musictechlab.io"," and tell us:",[845,3829,3830,3833,3836],{},[848,3831,3832],{},"Which feature would you use most?",[848,3834,3835],{},"What's missing that would make MemoSonic perfect for you?",[848,3837,3838],{},"Any category ideas we haven't thought of?",[743,3840,3841],{},"The best features come from real users with real needs. Don't be shy - your idea might end up in the next update.",[1292,3843],{},[750,3845,3847],{"id":3846},"the-dream-memosonic-as-a-physical-toy","The Dream: MemoSonic as a Physical Toy",[743,3849,3850],{},"Here's an idea that won't leave our heads...",[743,3852,3853],{},[805,3854,3855],{},"What if MemoSonic wasn't just an app, but a physical toy you could hold in your hands?",[755,3857,1276,3859,1276,3862,1276,3867],{"className":3858},[1272,1273,1274,1275],[755,3860],{"className":3861},[1280],[1282,3863],{"src":3864,"alt":3865,"className":3866},"/images/blog/musictechlab_blog_how-we-built-memosonic_inline_9.webp","Hardware concept",[1287],[755,3868],{"className":3869},[1280],[1325,3871,3873],{"id":3872},"the-vision","The Vision",[743,3875,3876],{},"Picture a compact device with a grid of large, tactile buttons. Each button:",[845,3878,3879,3882,3885,3888],{},[848,3880,3881],{},"Lights up with RGB LEDs",[848,3883,3884],{},"Plays a sound when pressed",[848,3886,3887],{},"Has a satisfying click",[848,3889,3890],{},"Is large enough for small hands (and accessible for everyone)",[743,3892,3893],{},"An 8×8 matrix would give us 64 buttons - enough for complex games while keeping each button big enough to press comfortably:",[1448,3895,3898],{"className":3896,"code":3897,"language":1453},[1451],"[🔘] [🔘] [🔘] [🔘] [🔘] [🔘] [🔘] [🔘]\n[🔘] [🔘] [🔘] [🔘] [🔘] [🔘] [🔘] [🔘]\n[🔘] [🔘] [🔘] [🔘] [🔘] [🔘] [🔘] [🔘]\n[🔘] [🔘] [🔘] [🔘] [🔘] [🔘] [🔘] [🔘]\n[🔘] [🔘] [🔘] [🔘] [🔘] [🔘] [🔘] [🔘]\n[🔘] [🔘] [🔘] [🔘] [🔘] [🔘] [🔘] [🔘]\n[🔘] [🔘] [🔘] [🔘] [🔘] [🔘] [🔘] [🔘]\n[🔘] [🔘] [🔘] [🔘] [🔘] [🔘] [🔘] [🔘]\n",[1455,3899,3897],{"__ignoreMap":887},[743,3901,3902],{},"Easy mode? Use a 3×2 section. Hard mode? The whole grid lights up.",[1325,3904,3906],{"id":3905},"why-does-this-idea-excite-us","Why Does This Idea Excite Us?",[743,3908,3909,3912],{},[805,3910,3911],{},"Screen-free play."," Parents increasingly want toys that don't involve staring at a screen. A physical MemoSonic would sit on the kitchen table, travel in a backpack, work without WiFi.",[743,3914,3915,3918,3919,3922],{},[805,3916,3917],{},"True tactile accessibility."," For visually impaired users, physical buttons in fixed positions are ",[1263,3920,3921],{},"far"," easier to navigate than a touchscreen. You can feel your way around. Build muscle memory. No screen reader required.",[743,3924,3925,3928],{},[805,3926,3927],{},"Multiplayer without devices."," Gather around the table. Take turns. Compete. No \"pass the phone\" awkwardness.",[743,3930,3931,3934],{},[805,3932,3933],{},"Durability."," Kids are rough. A well-designed hardware toy can survive drops, spills, and sibling conflicts.",[1325,3936,3938],{"id":3937},"what-it-could-look-like-technically","What It Could Look Like Technically",[743,3940,3941],{},"If we were to build this, we'd probably explore:",[845,3943,3944,3950,3956,3962,3968,3974],{},[848,3945,3946,3949],{},[805,3947,3948],{},"ESP32 or Raspberry Pi Pico"," as the brain",[848,3951,3952,3955],{},[805,3953,3954],{},"I2S audio"," for quality sound output",[848,3957,3958,3961],{},[805,3959,3960],{},"NeoPixel/WS2812B"," LEDs for button illumination",[848,3963,3964,3967],{},[805,3965,3966],{},"Rechargeable battery"," with USB-C charging",[848,3969,3970,3973],{},[805,3971,3972],{},"SD card slot"," for custom sound packs",[848,3975,3976,3979],{},[805,3977,3978],{},"Bluetooth LE"," for optional app companion (stats, new sounds)",[743,3981,3982],{},"The app and hardware could even sync - unlock new sounds on the device by mastering them in the app first.",[1325,3984,3986],{"id":3985},"the-accessibility-angle","The Accessibility Angle",[743,3988,3989],{},"This is where it gets really exciting.",[743,3991,3992],{},"For a blind child, a touchscreen memory game is possible but challenging. A physical device with buttons in consistent positions? That's a game they can master as well as any sighted player. Maybe better - they've been training their auditory memory their whole life.",[743,3994,3995],{},"We imagine:",[845,3997,3998,4004,4010,4016],{},[848,3999,4000,4003],{},[805,4001,4002],{},"Raised symbols"," on each button for tactile identification",[848,4005,4006,4009],{},[805,4007,4008],{},"Audio position announcements"," (\"Button 3\" when pressed)",[848,4011,4012,4015],{},[805,4013,4014],{},"Haptic feedback"," for matches and mismatches",[848,4017,4018,4021],{},[805,4019,4020],{},"Braille labeling"," on the device",[1325,4023,4025],{"id":4024},"is-this-something-youd-want","Is This Something You'd Want?",[743,4027,4028],{},"Right now, this is just an idea - a dream sketched on whiteboards and discussed over coffee. We haven't built a prototype yet. We're not sure if we will.",[743,4030,4031],{},[805,4032,4033],{},"But we could, if there's real interest.",[743,4035,4036],{},"Would you buy a physical MemoSonic for your kids? For a classroom? For a visually impaired family member? Would you back it on Kickstarter?",[743,4038,4039,4042],{},[805,4040,4041],{},"Let us know."," If enough people say \"yes, build this thing!\" - we just might.",[743,4044,4045,4046,4048],{},"📧 ",[827,4047,3826],{"href":3825}," - subject line: \"Hardware MemoSonic\"",[743,4050,4051],{},[1263,4052,4053],{},"If the response is strong enough, we'll start prototyping and document the entire journey. Stay tuned.",[1292,4055],{},[750,4057,4059],{"id":4058},"try-it-yourself","Try It Yourself",[743,4061,4062],{},"MemoSonic is available now:",[845,4064,4065,4076,4086],{},[848,4066,4067,4070,4071],{},[805,4068,4069],{},"Web",": ",[827,4072,4075],{"href":4073,"rel":4074},"https://memosonic.musictechlab.io/",[831],"memosonic.musictechlab.io",[848,4077,4078,4070,4081],{},[805,4079,4080],{},"iOS",[827,4082,4085],{"href":4083,"rel":4084},"https://apps.apple.com/pl/app/memosonic/id6743499949",[831],"App Store",[848,4087,4088,4070,4091],{},[805,4089,4090],{},"Android",[827,4092,4095],{"href":4093,"rel":4094},"https://play.google.com/store/apps/details?id=com.memosonic.app",[831],"Google Play",[743,4097,4098],{},"Whether you're a music teacher looking for ear training tools, a parent wanting educational screen time, or someone who just wants to see if they can beat their kids at a memory game - give it a try.",[1292,4100],{},[750,4102,4104],{"id":4103},"the-bigger-picture","The Bigger Picture",[743,4106,4107],{},"MemoSonic started as a rainy day experiment. It became something more - a proof that:",[1420,4109,4110,4116,4122,4128],{},[848,4111,4112,4115],{},[805,4113,4114],{},"Learning doesn't have to look like learning."," The best educational tools feel like play.",[848,4117,4118,4121],{},[805,4119,4120],{},"Accessibility opens doors."," By designing for sound-first gameplay, we accidentally created something that works for people we hadn't initially considered.",[848,4123,4124,4127],{},[805,4125,4126],{},"Simple ideas can have big impact."," Flip cards, match sounds. That's it. But the applications - music education, auditory training, inclusive gaming - are vast.",[848,4129,4130,4133],{},[805,4131,4132],{},"Software is just the beginning."," The same concept that works on a phone could work on a physical device - maybe even better for some users.",[743,4135,4136],{},"Sometimes the best projects come from playing with your kids.",[1292,4138],{},[750,4140,4142],{"id":4141},"resources","Resources",[845,4144,4145,4152,4159,4166],{},[848,4146,4147],{},[827,4148,4151],{"href":4149,"rel":4150},"https://flutter.dev",[831],"Flutter Documentation",[848,4153,4154],{},[827,4155,4158],{"href":4156,"rel":4157},"https://pub.dev/packages/just_audio",[831],"just_audio Package",[848,4160,4161],{},[827,4162,4165],{"href":4163,"rel":4164},"https://www.w3.org/WAI/standards-guidelines/mobile/",[831],"Accessibility Guidelines for Mobile Apps",[848,4167,4168],{},[827,4169,4172],{"href":4170,"rel":4171},"https://docs.espressif.com/projects/esp-adf/en/latest/",[831],"ESP32 Audio Projects",[1292,4174],{},[750,4176,4178],{"id":4177},"for-clients-what-to-know-before-commissioning-a-mobile-app","For Clients: What to Know Before Commissioning a Mobile App",[743,4180,4181],{},"We thought it might be useful to share some honest insights from building MemoSonic - especially if you're considering commissioning a mobile app yourself.",[1325,4183,4185],{"id":4184},"timeline-reality","Timeline Reality",[743,4187,4188,4191,4192,1415],{},[805,4189,4190],{},"MemoSonic from idea to production:"," ~10 calendar months, but effectively ",[805,4193,4194],{},"2-3 weeks of intense work",[743,4196,4197],{},"Why the difference? Because projects have their own rhythm - there are pauses for testing ideas, gathering feedback, handling other priorities. Realistically:",[1151,4199,4200,4210],{},[1154,4201,4202],{},[1157,4203,4204,4207],{},[1160,4205,4206],{},"Phase",[1160,4208,4209],{},"Time",[1165,4211,4212,4220,4228,4235,4243],{},[1157,4213,4214,4217],{},[1170,4215,4216],{},"Prototype / proof of concept",[1170,4218,4219],{},"1-2 days",[1157,4221,4222,4225],{},[1170,4223,4224],{},"Core functionality",[1170,4226,4227],{},"3-5 days",[1157,4229,4230,4233],{},[1170,4231,4232],{},"UI/UX polish",[1170,4234,4227],{},[1157,4236,4237,4240],{},[1170,4238,4239],{},"Testing, bugs, deployment",[1170,4241,4242],{},"2-5 days",[1157,4244,4245,4250],{},[1170,4246,4247],{},[805,4248,4249],{},"Effective total",[1170,4251,4252],{},[805,4253,4254],{},"2-3 weeks",[1325,4256,4258],{"id":4257},"where-do-most-problems-occur","Where Do Most Problems Occur?",[743,4260,4261,4264],{},[805,4262,4263],{},"1. Audio/Media"," - Sounds simple (\"just play a sound\"), but:",[845,4266,4267,4270,4273,4276],{},[848,4268,4269],{},"Playback latency issues",[848,4271,4272],{},"Conflicts when sounds overlap",[848,4274,4275],{},"Differences between iOS and Android behavior",[848,4277,4278],{},"Library bugs (e.g., \"Message responses can be sent only once\")",[743,4280,4281,4284],{},[805,4282,4283],{},"2. iOS Builds"," - Every mobile project confirms this. Certificates, provisioning profiles, App Store review. Android is simpler.",[743,4286,4287,4290],{},[805,4288,4289],{},"3. Content and Copyright"," - Want to use a famous melody? Images from the internet? You either generate your own assets or buy licenses. (We had to remove Metallica from the project for this reason.)",[1325,4292,4294],{"id":4293},"simple-ideas-complex-threads","Simple Ideas → Complex Threads",[743,4296,4297],{},"MemoSonic is \"just\" a memory game with sounds. Sounds like a weekend project, right?",[743,4299,4300],{},"And yet:",[845,4302,4303,4309,4315,4321,4327,4333],{},[848,4304,4305,4308],{},[805,4306,4307],{},"300+ audio files"," to generate (chords, scales, rhythms, animal sounds)",[848,4310,4311,4314],{},[805,4312,4313],{},"Content generation pipeline"," - Python, FluidSynth, FFmpeg, sound synthesis",[848,4316,4317,4320],{},[805,4318,4319],{},"Two game modes"," with different logic",[848,4322,4323,4326],{},[805,4324,4325],{},"Three difficulty levels"," with balancing",[848,4328,4329,4332],{},[805,4330,4331],{},"Accessibility"," - contrast, large buttons, audio feedback",[848,4334,4335,4338],{},[805,4336,4337],{},"Card animations"," with 3D perspective",[743,4340,4341],{},"What seems like a \"simple app\" often requires:",[845,4343,4344,4347,4350,4353],{},[848,4345,4346],{},"Integration with multiple libraries",[848,4348,4349],{},"Handling edge cases",[848,4351,4352],{},"Testing on various devices",[848,4354,4355],{},"Iterations based on user feedback",[1325,4357,4359],{"id":4358},"the-golden-rule","The Golden Rule",[797,4361,4362],{},[743,4363,4364],{},[805,4365,4366],{},"MVP in 5 days, polishing - endless.",[743,4368,4369],{},"The first working version comes together quickly. But the difference between \"it works\" and \"it works well on every device, looks professional, and doesn't crash\" - that's weeks of additional work.",[1325,4371,4373],{"id":4372},"the-bottom-line","The Bottom Line",[743,4375,4376],{},"If you're planning to build an app, budget for:",[845,4378,4379,4385,4391,4397],{},[848,4380,4381,4384],{},[805,4382,4383],{},"Time:"," 2-3x your initial estimate",[848,4386,4387,4390],{},[805,4388,4389],{},"Complexity:"," Simple features often hide complex implementations",[848,4392,4393,4396],{},[805,4394,4395],{},"Platform quirks:"," iOS, Android and web behave differently",[848,4398,4399,4402],{},[805,4400,4401],{},"Content:"," Creating or licensing assets takes time and money",[743,4404,4405,4406,4409],{},"The silver lining? ",[805,4407,4408],{},"Cross-platform frameworks like Flutter"," mean you get both iOS and Android from a single codebase. One team, one codebase, two app stores. That's a massive time and cost saver compared to building native apps separately.",[743,4411,4412],{},"The good news? With the right team and realistic expectations, even ambitious ideas can become polished products. MemoSonic started as a rainy Sunday vibe-coded game with kids. Now it's a  people train their ears.",[1292,4414],{},[1325,4416,4418],{"id":4417},"try-memosonic","Try MemoSonic",[845,4420,4421,4430,4438],{},[848,4422,4423,4426,4427],{},[805,4424,4425],{},"Web:"," ",[827,4428,4075],{"href":4073,"rel":4429},[831],[848,4431,4432,4426,4435],{},[805,4433,4434],{},"iOS:",[827,4436,4085],{"href":4083,"rel":4437},[831],[848,4439,4440,4426,4443],{},[805,4441,4442],{},"Android:",[827,4444,4095],{"href":4093,"rel":4445},[831],[4447,4448,4449],"style",{},"html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}",{"title":887,"searchDepth":888,"depth":888,"links":4451},[4452,4453,4456,4460,4468,4472,4476,4477,4481,4486,4492,4493,4500,4501,4502,4503],{"id":1254,"depth":888,"text":1255},{"id":1296,"depth":888,"text":1297,"children":4454},[4455],{"id":1327,"depth":1542,"text":1328},{"id":1379,"depth":888,"text":1380,"children":4457},[4458,4459],{"id":1383,"depth":1542,"text":1384},{"id":1407,"depth":1542,"text":1408},{"id":1461,"depth":888,"text":1462,"children":4461},[4462,4463,4464,4465,4466,4467],{"id":1486,"depth":1542,"text":1487},{"id":1797,"depth":1542,"text":1798},{"id":2387,"depth":1542,"text":2388},{"id":2639,"depth":1542,"text":2640},{"id":2870,"depth":1542,"text":2871},{"id":2964,"depth":1542,"text":2965},{"id":3005,"depth":888,"text":3006,"children":4469},[4470,4471],{"id":3012,"depth":1542,"text":3013},{"id":3052,"depth":1542,"text":3053},{"id":3068,"depth":888,"text":3069,"children":4473},[4474,4475],{"id":3075,"depth":1542,"text":3076},{"id":3102,"depth":1542,"text":3103},{"id":3117,"depth":888,"text":3118},{"id":3205,"depth":888,"text":3206,"children":4478},[4479,4480],{"id":3212,"depth":1542,"text":3213},{"id":3255,"depth":1542,"text":3256},{"id":3331,"depth":888,"text":3332,"children":4482},[4483,4484,4485],{"id":3335,"depth":1542,"text":3336},{"id":3345,"depth":1542,"text":3346},{"id":3441,"depth":1542,"text":3442},{"id":3686,"depth":888,"text":3687,"children":4487},[4488,4489,4490,4491],{"id":3690,"depth":1542,"text":3691},{"id":3697,"depth":1542,"text":3698},{"id":3715,"depth":1542,"text":3716},{"id":3722,"depth":1542,"text":3723},{"id":3748,"depth":888,"text":3749},{"id":3846,"depth":888,"text":3847,"children":4494},[4495,4496,4497,4498,4499],{"id":3872,"depth":1542,"text":3873},{"id":3905,"depth":1542,"text":3906},{"id":3937,"depth":1542,"text":3938},{"id":3985,"depth":1542,"text":3986},{"id":4024,"depth":1542,"text":4025},{"id":4058,"depth":888,"text":4059},{"id":4103,"depth":888,"text":4104},{"id":4141,"depth":888,"text":4142},{"id":4177,"depth":888,"text":4178,"children":4504},[4505,4506,4507,4508,4509,4510],{"id":4184,"depth":1542,"text":4185},{"id":4257,"depth":1542,"text":4258},{"id":4293,"depth":1542,"text":4294},{"id":4358,"depth":1542,"text":4359},{"id":4372,"depth":1542,"text":4373},{"id":4417,"depth":1542,"text":4418},{"name":4512,"logo":4513},"MusicTech Lab","/images/logos/memosonic-app.png","2026-01-08T00:00:00.000Z","The story behind MemoSonic, a Flutter-based educational game that turns sound recognition into play, with accessibility for visually impaired users.",{"src":4517},"/images/blog/musictechlab_blog_how-we-built-memosonic-accessible-audio-memory-game-flutter.webp",{"enabled":906,"items":4519},[4520,4523,4526,4529],{"text":4521,"icon":4522},"300+ audio assets generated programmatically using Python, FluidSynth, and FFmpeg.","i-lucide-terminal",{"text":4524,"icon":4525},"Sound-first gameplay makes the app accessible to visually impaired players by design.","i-lucide-headphones",{"text":4527,"icon":4528},"Effective development took 2-3 weeks despite a 10-month calendar timeline.","i-lucide-zap",{"text":4530,"icon":4531},"6 categories cover chords, scales, rhythms, notes, animals, and instruments.","i-lucide-music",{},"/blog/case-studies/how-we-built-memosonic-accessible-audio-memory-game-flutter",{"title":26,"description":4515},[897,4536],"development","EpQHhZ99WiwpfNwqAZy1Tpb3DU7mhR5z4TDWIwaKj0c",{"id":4539,"title":14,"authors":736,"badge":4540,"body":4541,"category":897,"client":4884,"date":4885,"description":4886,"extension":902,"faq":736,"featured":69,"featuredOrder":736,"hidden":69,"image":4887,"keyTakeaways":4889,"meta":4900,"navigation":906,"path":4901,"seo":4902,"status":736,"stem":16,"tags":4903,"teaser":736,"__hash__":4904,"score":1542},"posts/blog/case-study/chromecast-airplay-casting-app-case-study.md",{"label":5,"color":738},{"type":740,"value":4542,"toc":4871},[4543,4547,4550,4553,4556,4561,4565,4572,4575,4598,4601,4605,4608,4611,4627,4632,4636,4639,4642,4646,4649,4655,4658,4662,4679,4683,4714,4718,4721,4738,4743,4747,4811,4813,4849,4853],[750,4544,4546],{"id":4545},"the-brief-that-seemed-simple","The Brief That Seemed Simple",[743,4548,4549],{},"A media company came to us with what sounded like a straightforward request: \"We want users to browse video content on their phones and cast it to any TV nearby.\"",[743,4551,4552],{},"Simple, right? Tap a video, tap a TV, watch.",[743,4554,4555],{},"Except the word \"any\" hid a world of complexity. The client's users had Chromecasts, Apple TVs, smart TVs with AirPlay, and older devices. The content needed dynamic overlays — QR codes, logos, animations that appear at specific moments. And the whole experience had to feel instant, like the phone was just a fancy remote control.",[1007,4557,4558],{},[743,4559,4560],{},"We quickly realized this wasn't a development project. It was an R&D expedition.",[750,4562,4564],{"id":4563},"the-real-problem-control-not-just-playback","The Real Problem: Control, Not Just Playback",[743,4566,4567,4568,4571],{},"Here's what most people don't understand about Chromecast: playing a video is easy. ",[1263,4569,4570],{},"Controlling"," what happens on the TV is hard.",[743,4573,4574],{},"Chromecast was designed for a simple use case — send a URL, let the TV handle it. But our client needed more:",[755,4576,4578,4583,4588,4593],{"className":4577},[758,759,1030,761,762],[764,4579],{"description":4580,"icon":4581,"title":4582},"Toggle QR codes on and off during playback","i-lucide-qr-code","QR Code Overlays",[764,4584],{"description":4585,"icon":4586,"title":4587},"Show debug panels for testing","i-lucide-bug","Debug Information",[764,4589],{"description":4590,"icon":4591,"title":4592},"Display graphics at specific timestamps","i-lucide-layers","Custom Overlays",[764,4594],{"description":4595,"icon":4596,"title":4597},"Send commands the TV actually understands","i-lucide-send","Custom Commands",[743,4599,4600],{},"The default Chromecast receiver doesn't do any of that. We needed to build our own.",[750,4602,4604],{"id":4603},"the-platform-decision","The Platform Decision",[743,4606,4607],{},"Before writing any production code, we needed to answer a fundamental question: what technology could handle casting to multiple ecosystems while maintaining a single codebase?",[743,4609,4610],{},"We built proof-of-concept apps in three different frameworks:",[755,4612,4614,4619,4623],{"className":4613},[758,759,760,761,762],[764,4615],{"description":4616,"icon":4617,"title":4618},"Huge ecosystem, but Chromecast libraries were outdated and unreliable. After two weeks we had an app that sometimes found nearby devices. Sometimes.","i-lucide-x-circle","React Native",[764,4620],{"description":4621,"icon":4617,"title":4622},"Best casting integration, but maintaining two codebases for a startup budget wasn't realistic. More time syncing features than building them.","Native Swift/Kotlin",[764,4624],{"description":4625,"icon":4626,"title":1403},"Google's backing meant solid Chromecast support. Plugin architecture let us write native casting code when needed, keeping 90% cross-platform.","i-lucide-check-circle",[787,4628,4629],{},[743,4630,4631],{},"We went with Flutter. It was a bet, but one that paid off.",[750,4633,4635],{"id":4634},"the-casting-puzzle-14-prototypes-later","The Casting Puzzle: 14 Prototypes Later",[743,4637,4638],{},"Here's something we don't usually admit in case studies: we built fourteen different prototypes before landing on the final architecture.",[1020,4640],{":items":4641},"[{\"title\":\"WebRTC — The Dead End\",\"description\":\"Peer-to-peer video streaming seemed ideal. We built a Node.js signaling server and got devices talking. But casting isn't a conversation — it's a broadcast. The TV doesn't need to send video back. We were overcomplicating things. Not wasted though — we extracted the QR code pairing system that became a core feature.\",\"icon\":\"i-lucide-git-branch\"},{\"title\":\"FFmpeg — The Detour\",\"description\":\"Server-side video processing to bake overlays into the stream. Built two versions (Python FastAPI + embedded Flutter). Both worked, but processing delay was noticeable and every overlay change meant re-encoding. For hundreds of videos, not sustainable.\",\"icon\":\"i-lucide-film\"},{\"title\":\"Client-Side Rendering — The Insight\",\"description\":\"Overlays needed to be rendered in real-time on top of the video stream. More complex to build, infinitely more flexible. No re-encoding, no delay.\",\"icon\":\"i-lucide-lightbulb\"},{\"title\":\"Multi-Layer Architecture — The Breakthrough\",\"description\":\"Instead of one video with baked-in graphics, a layered system: base video, overlay layer (QR codes, logos), animation layer (Lottie), subtitle layer, and audio mixing. It worked on the phone — but could we cast this to a TV?\",\"icon\":\"i-lucide-layers\"},{\"title\":\"Custom Chromecast Receiver — The Solution\",\"description\":\"We registered our own receiver with Google and built a web app running on the Chromecast itself. Custom namespace messaging, instant overlay commands, and full visual control — without touching Chromecast firmware.\",\"icon\":\"i-lucide-cast\"}]",[750,4643,4645],{"id":4644},"the-airplay-surprise","The AirPlay Surprise",[743,4647,4648],{},"AirPlay should have been easier — Apple's ecosystem, tight integration, \"it just works.\"",[743,4650,4651,4652],{},"Except Flutter had no official AirPlay plugin. ",[805,4653,4654],{},"We wrote our own.",[743,4656,4657],{},"Three weeks of diving into Apple's documentation, bridging Swift code to Dart, and testing on every AirPlay device we could find. This wasn't in the original scope, but without it, we'd have lost half our potential users.",[750,4659,4661],{"id":4660},"the-problems-nobody-warned-us-about","The Problems Nobody Warned Us About",[755,4663,4665,4670,4674],{"className":4664},[758,759,760,761,762],[764,4666],{"description":4667,"icon":4668,"title":4669},"Chromecast uses mDNS. AirPlay uses mDNS differently. Some networks block multicast. Corporate WiFi isolates devices. We built a multi-protocol discovery system with caching and graceful reconnection.","i-lucide-wifi","Network Discovery is Chaos",[764,4671],{"description":4672,"icon":1231,"title":4673},"Phone and TV are separate devices with separate states. We built a system treating the phone as source of truth for intent while respecting the TV's reality. When they diverge, graceful reconciliation.","State Synchronization",[764,4675],{"description":4676,"icon":4677,"title":4678},"A developer accidentally streamed 2GB over cellular. We added network awareness — detect WiFi-to-mobile switches, warn users, optionally pause. Small feature, saved users real money.","i-lucide-signal","The Mobile Data Problem",[750,4680,4682],{"id":4681},"what-we-actually-built","What We Actually Built",[755,4684,4686,4691,4696,4700,4705,4710],{"className":4685},[758,759,1030,761,762],[764,4687],{"description":4688,"icon":4689,"title":4690},"A web app running on the Chromecast with our overlay system, custom commands, and on-demand QR code rendering.","i-lucide-cast","Custom Chromecast Receiver",[764,4692],{"description":4693,"icon":4694,"title":4695},"Chromecast and AirPlay from the same interface. Users don't need to know which protocol their TV uses.","i-lucide-airplay","Universal Casting",[764,4697],{"description":4698,"icon":4591,"title":4699},"QR codes, logos, and animations toggled instantly. No re-encoding, no delay.","Real-Time Overlay Control",[764,4701],{"description":4702,"icon":4703,"title":4704},"Content organized into channels with D-pad navigation. Swipe up for next channel, swipe right for next video.","i-lucide-tv","Channel-Based Browsing",[764,4706],{"description":4707,"icon":4708,"title":4709},"Start on phone, cast to TV, pick up phone later — it knows what's playing. Always feels like a remote control.","i-lucide-repeat","Seamless Handoff",[764,4711],{"description":4712,"icon":4586,"title":4713},"Toggle a debug panel on the TV from your phone. Essential for testing, easy to disable in production.","Debug Mode",[750,4715,4717],{"id":4716},"the-rd-investment","The R&D Investment",[743,4719,4720],{},"Fourteen prototypes sounds like waste. It wasn't. Each failed approach taught us something:",[845,4722,4723,4726,4729,4732,4735],{},[848,4724,4725],{},"WebRTC showed us the value of QR pairing",[848,4727,4728],{},"FFmpeg experiments proved client-side rendering was necessary",[848,4730,4731],{},"React Native's limitations confirmed Flutter was the right choice",[848,4733,4734],{},"The multi-layer PoC became the foundation of our overlay system",[848,4736,4737],{},"Early Chromecast tests revealed we needed a custom receiver",[1007,4739,4740],{},[743,4741,4742],{},"The client got a production app, but they also got certainty. We didn't guess at the architecture — we proved it through systematic exploration.",[750,4744,4746],{"id":4745},"lessons-for-clients","Lessons for Clients",[1151,4748,4749,4759],{},[1154,4750,4751],{},[1157,4752,4753,4756],{},[1160,4754,4755],{},"Insight",[1160,4757,4758],{},"Why it matters",[1165,4760,4761,4771,4781,4791,4801],{},[1157,4762,4763,4768],{},[1170,4764,4765],{},[805,4766,4767],{},"Casting isn't streaming",[1170,4769,4770],{},"Sending video to a TV is easy. Controlling what happens on that TV requires custom development",[1157,4772,4773,4778],{},[1170,4774,4775],{},[805,4776,4777],{},"Custom receivers unlock everything",[1170,4779,4780],{},"Anything beyond play/pause/seek means building your own Chromecast receiver",[1157,4782,4783,4788],{},[1170,4784,4785],{},[805,4786,4787],{},"Platform choice matters enormously",[1170,4789,4790],{},"Two weeks on React Native felt like lost time, but it would have been months of pain",[1157,4792,4793,4798],{},[1170,4794,4795],{},[805,4796,4797],{},"Prototypes aren't waste",[1170,4799,4800],{},"Every PoC either became part of the final product or eliminated a dead end early",[1157,4802,4803,4808],{},[1170,4804,4805],{},[805,4806,4807],{},"Test on real hardware",[1170,4809,4810],{},"Chromecast bugs don't show up in simulators. We maintained a \"casting corner\" with multiple device generations",[750,4812,1064],{"id":1063},[755,4814,4816,4819,4823,4827,4832,4837,4842,4846],{"className":4815},[758,1068,760,1069,1070,762],[764,4817],{"description":4818,"icon":958,"title":1403},"Cross-platform mobile",[764,4820],{"description":4821,"icon":4689,"title":4822},"Custom receiver app","Chromecast SDK",[764,4824],{"description":4825,"icon":4694,"title":4826},"Custom Flutter plugin","AirPlay",[764,4828],{"description":4829,"icon":4830,"title":4831},"QR-based device pairing","i-lucide-video","WebRTC",[764,4833],{"description":4834,"icon":4835,"title":4836},"Video processing","i-lucide-film","FFmpeg",[764,4838],{"description":4839,"icon":4840,"title":4841},"Animation overlays","i-lucide-sparkles","Lottie",[764,4843],{"description":4844,"icon":953,"title":4845},"Signaling server","Node.js",[764,4847],{"description":4848,"icon":4528,"title":775},"Video processing API",[750,4850,4852],{"id":4851},"deliverables","Deliverables",[755,4854,4856,4860,4863,4867],{"className":4855},[758,759,1030,761,762],[764,4857],{"description":4858,"icon":958,"title":4859},"iOS + Android from a single Flutter codebase","Cross-Platform Mobile App",[764,4861],{"description":4862,"icon":4689,"title":4690},"With overlay support and custom messaging protocol",[764,4864],{"description":4865,"icon":4694,"title":4866},"Built from scratch for Flutter","Custom AirPlay Plugin",[764,4868],{"description":4869,"icon":1059,"title":4870},"Content and overlay management","Backend CMS",{"title":887,"searchDepth":888,"depth":888,"links":4872},[4873,4874,4875,4876,4877,4878,4879,4880,4881,4882,4883],{"id":4545,"depth":888,"text":4546},{"id":4563,"depth":888,"text":4564},{"id":4603,"depth":888,"text":4604},{"id":4634,"depth":888,"text":4635},{"id":4644,"depth":888,"text":4645},{"id":4660,"depth":888,"text":4661},{"id":4681,"depth":888,"text":4682},{"id":4716,"depth":888,"text":4717},{"id":4745,"depth":888,"text":4746},{"id":1063,"depth":888,"text":1064},{"id":4851,"depth":888,"text":4852},{"nda":906},"2025-01-13T00:00:00.000Z","How controlling a TV turned out to be harder than streaming to it, and why we built our own Chromecast receiver after 14 prototypes.",{"src":4888,"hasLogo":69},"/images/blog/chromecast-airplay-casting-app-case-study.webp",{"enabled":906,"items":4890},[4891,4894,4896,4898],{"text":4892,"icon":4893},"14 prototypes built before finding the right casting architecture.","i-lucide-blocks",{"text":4895,"icon":4689},"Custom Chromecast receiver enables real-time QR code and animation overlays on TV.",{"text":4897,"icon":910},"Custom AirPlay Flutter plugin written from scratch in 3 weeks.",{"text":4899,"icon":958},"Flutter chosen over React Native and native after PoC comparison across all three.",{},"/blog/case-studies/chromecast-airplay-casting-app-case-study",{"title":14,"description":4886},[897,4536],"rp1YICin9SG9v9J2bLejUif7lpAB5aZe85kWeFkRR6A",{"id":4906,"title":38,"authors":736,"badge":4907,"body":4908,"category":897,"client":6260,"date":4885,"description":6263,"extension":902,"faq":736,"featured":906,"featuredOrder":6264,"hidden":69,"image":6265,"keyTakeaways":6267,"meta":6279,"navigation":906,"path":6280,"seo":6281,"status":736,"stem":40,"tags":6282,"teaser":736,"__hash__":6284,"score":1542},"posts/blog/case-study/musicdata-lab-universal-music-data-parser-case-study.md",{"label":5,"color":738},{"type":740,"value":4909,"toc":6226},[4910,4915,4919,4929,4932,4938,4952,4955,4958,4962,4965,5055,5058,5062,5065,5069,5072,5075,5079,5082,5099,5103,5106,5120,5262,5271,5280,5284,5291,5295,5403,5407,5499,5503,5529,5533,5536,5625,5634,5643,5647,5673,5677,5680,5700,5709,5713,5762,5765,5791,5823,5827,5830,5892,5901,5905,5909,5916,5989,5993,5996,6002,6007,6016,6020,6023,6025,6052,6056,6059,6090,6099,6108,6117,6121,6124,6143,6150,6153,6157,6185,6189,6192,6223],[1007,4911,4912],{},[743,4913,4914],{},"This case study describes our work with CherryRed Records. Some technical details have been generalized to protect proprietary systems.",[750,4916,4918],{"id":4917},"the-problem-data-chaos-in-music-industry","The Problem: Data Chaos in Music Industry",[755,4920,4922],{"className":4921},[1272,1275,762],[743,4923,4924],{},[1282,4925],{"alt":4926,"src":4927,"width":4928},"MusicData Lab - login page with CherryRed Records branding","/images/blog/musicdatalab/login.webp",800,[743,4930,4931],{},"It was 2023 when we first sat down with a UK music label facing a common but frustrating problem. Every month, they received royalty reports from their distributors - ADA (Warner), The Orchard (Sony), Merlin, FUGA, Bandcamp, and a dozen others. Each report contained the same basic information: how many times their artists' songs were streamed, and how much money they earned.",[4933,4934,4935],"warning",{},[743,4936,4937],{},"Simple, right? Not quite.",[743,4939,4940,4943,4944,4947,4948,4951],{},[805,4941,4942],{},"ADA"," sent Excel files with columns named \"Release Artist\", \"Release Title\", and \"Net Payable\". ",[805,4945,4946],{},"Merlin"," called them \"Artist name\", \"Track name\", and \"Payable\". ",[805,4949,4950],{},"The Orchard"," went with \"Artist Name\", \"Track Name\", and \"Label Share Net Receipts\".",[743,4953,4954],{},"Same data. Different languages.",[743,4956,4957],{},"And that was just the beginning.",[1325,4959,4961],{"id":4960},"the-date-format-nightmare","The Date Format Nightmare",[743,4963,4964],{},"Consider how different distributors format dates:",[1151,4966,4967,4980],{},[1154,4968,4969],{},[1157,4970,4971,4974,4977],{},[1160,4972,4973],{},"Distributor",[1160,4975,4976],{},"Date Format",[1160,4978,4979],{},"Example",[1165,4981,4982,4994,5007,5019,5029,5042],{},[1157,4983,4984,4986,4989],{},[1170,4985,4942],{},[1170,4987,4988],{},"Excel serial number",[1170,4990,4991],{},[1455,4992,4993],{},"45292",[1157,4995,4996,4999,5002],{},[1170,4997,4998],{},"ADA (variant)",[1170,5000,5001],{},"YYYYMM integer",[1170,5003,5004],{},[1455,5005,5006],{},"202501",[1157,5008,5009,5011,5014],{},[1170,5010,4950],{},[1170,5012,5013],{},"\"YYYYMNN\"",[1170,5015,5016],{},[1455,5017,5018],{},"\"2025M01\"",[1157,5020,5021,5023,5026],{},[1170,5022,4946],{},[1170,5024,5025],{},"Hidden in metadata cells",[1170,5027,5028],{},"Row 1, Column B",[1157,5030,5031,5034,5037],{},[1170,5032,5033],{},"Qello",[1170,5035,5036],{},"Embedded in filename",[1170,5038,5039],{},[1455,5040,5041],{},"Qello_20250101_20250331.xlsx",[1157,5043,5044,5047,5050],{},[1170,5045,5046],{},"Believe",[1170,5048,5049],{},"ISO date string",[1170,5051,5052],{},[1455,5053,5054],{},"\"2025-01-15\"",[743,5056,5057],{},"One distributor. Four date formats. And we had 20+ distributors to support.",[1325,5059,5061],{"id":5060},"the-currency-problem","The Currency Problem",[743,5063,5064],{},"Some distributors report in USD. Others in EUR, GBP, or the original local currency. Some files contain mixed currencies. Some hardcode \"USD\" even when the actual currency varies by territory.",[1325,5066,5068],{"id":5067},"the-same-but-different-problem","The \"Same But Different\" Problem",[743,5070,5071],{},"\"UNITED STATES\" vs \"USA\" vs \"US\" vs \"United States of America\". All the same country. All different strings that need to be normalized.",[743,5073,5074],{},"\"Sony Music\" vs \"SONY\" vs \"Sony Music Entertainment\" vs \"SME\". Same label. Four different spellings across different platforms.",[750,5076,5078],{"id":5077},"the-solution-adapter-pattern-with-field-mapping","The Solution: Adapter Pattern with Field Mapping",[743,5080,5081],{},"We needed a system that could:",[1420,5083,5084,5087,5090,5093,5096],{},[848,5085,5086],{},"Accept any file format (CSV, XLS, XLSX, XLSB)",[848,5088,5089],{},"Map arbitrary column names to a standard schema",[848,5091,5092],{},"Handle date parsing edge cases per distributor",[848,5094,5095],{},"Normalize currencies, countries, and entity names",[848,5097,5098],{},"Scale to hundreds of millions of records",[1325,5100,5102],{"id":5101},"the-architecture","The Architecture",[743,5104,5105],{},"At its core, the system uses the Adapter Pattern - a universal base class that handles all common operations, with specialized adapters for each distributor that define only what's different:",[845,5107,5108,5114],{},[848,5109,5110,5113],{},[805,5111,5112],{},"Base MDLImporter"," - handles file parsing, chunked processing, country normalization, currency conversion, and Elasticsearch indexing",[848,5115,5116,5119],{},[805,5117,5118],{},"Distributor Adapters"," - each adapter contains only a field mapping dictionary and any date/currency parsing overrides specific to that distributor",[1448,5121,5124],{"className":1513,"code":5122,"filename":5123,"language":1515,"meta":887,"style":887},"class ADAAdapter(BaseMDLImporter):\n    FIELD_MAP = {\n        \"Release Artist\": \"artist\",\n        \"Release Title\":  \"track\",\n        \"Net Payable\":    \"income\",\n    }\n\n    def parse_date(self, raw):\n        # ADA uses Excel serial numbers\n        return excel_serial_to_date(raw)\n","adapters/ada_adapter.py",[1455,5125,5126,5141,5150,5171,5192,5212,5217,5221,5242,5247],{"__ignoreMap":887},[1519,5127,5128,5131,5134,5136,5139],{"class":1521,"line":1522},[1519,5129,5130],{"class":1814},"class",[1519,5132,5133],{"class":2778}," ADAAdapter",[1519,5135,1822],{"class":1535},[1519,5137,5138],{"class":2778},"BaseMDLImporter",[1519,5140,1834],{"class":1535},[1519,5142,5143,5146,5148],{"class":1521,"line":888},[1519,5144,5145],{"class":1531},"    FIELD_MAP ",[1519,5147,1536],{"class":1535},[1519,5149,1539],{"class":1535},[1519,5151,5152,5155,5158,5160,5162,5164,5167,5169],{"class":1521,"line":1542},[1519,5153,5154],{"class":1535},"        \"",[1519,5156,5157],{"class":1548},"Release Artist",[1519,5159,1552],{"class":1535},[1519,5161,1555],{"class":1535},[1519,5163,2107],{"class":1535},[1519,5165,5166],{"class":1548},"artist",[1519,5168,1552],{"class":1535},[1519,5170,3478],{"class":1535},[1519,5172,5173,5175,5178,5180,5182,5185,5188,5190],{"class":1521,"line":1241},[1519,5174,5154],{"class":1535},[1519,5176,5177],{"class":1548},"Release Title",[1519,5179,1552],{"class":1535},[1519,5181,1555],{"class":1535},[1519,5183,5184],{"class":1535},"  \"",[1519,5186,5187],{"class":1548},"track",[1519,5189,1552],{"class":1535},[1519,5191,3478],{"class":1535},[1519,5193,5194,5196,5199,5201,5203,5205,5208,5210],{"class":1521,"line":1611},[1519,5195,5154],{"class":1535},[1519,5197,5198],{"class":1548},"Net Payable",[1519,5200,1552],{"class":1535},[1519,5202,1555],{"class":1535},[1519,5204,1545],{"class":1535},[1519,5206,5207],{"class":1548},"income",[1519,5209,1552],{"class":1535},[1519,5211,3478],{"class":1535},[1519,5213,5214],{"class":1521,"line":1645},[1519,5215,5216],{"class":1535},"    }\n",[1519,5218,5219],{"class":1521,"line":1678},[1519,5220,1946],{"emptyLinePlaceholder":906},[1519,5222,5223,5226,5229,5231,5235,5237,5240],{"class":1521,"line":1712},[1519,5224,5225],{"class":1814},"    def",[1519,5227,5228],{"class":1818}," parse_date",[1519,5230,1822],{"class":1535},[1519,5232,5234],{"class":5233},"s5tWE","self",[1519,5236,1565],{"class":1535},[1519,5238,5239],{"class":1825}," raw",[1519,5241,1834],{"class":1535},[1519,5243,5244],{"class":1521,"line":1742},[1519,5245,5246],{"class":1525},"        # ADA uses Excel serial numbers\n",[1519,5248,5249,5252,5255,5257,5260],{"class":1521,"line":1772},[1519,5250,5251],{"class":1839},"        return",[1519,5253,5254],{"class":1818}," excel_serial_to_date",[1519,5256,1822],{"class":1535},[1519,5258,5259],{"class":1818},"raw",[1519,5261,1941],{"class":1535},[787,5263,5264],{},[743,5265,5266,5267,5270],{},"This approach means adding a new distributor typically takes just ",[805,5268,5269],{},"2-3 hours"," of work - define the field mapping, handle any date/currency quirks, and you're done.",[755,5272,5274],{"className":5273},[1272,1275,762],[743,5275,5276],{},[1282,5277],{"alt":5278,"src":5279,"width":4928},"Adapter configuration - field mapping between distributor columns and the universal schema","/images/blog/musicdatalab/adapters_conf.webp",[750,5281,5283],{"id":5282},"supported-distributors-and-adapters","Supported Distributors and Adapters",[743,5285,5286,5287,5290],{},"We currently support ",[805,5288,5289],{},"25+ adapters"," covering the major music distribution ecosystem:",[1325,5292,5294],{"id":5293},"major-distributors","Major Distributors",[1151,5296,5297,5309],{},[1154,5298,5299],{},[1157,5300,5301,5303,5306],{},[1160,5302,4973],{},[1160,5304,5305],{},"Parent Company",[1160,5307,5308],{},"Adapter Variants",[1165,5310,5311,5323,5335,5347,5360,5370,5381,5392],{},[1157,5312,5313,5317,5320],{},[1170,5314,5315],{},[805,5316,4942],{},[1170,5318,5319],{},"Warner Music",[1170,5321,5322],{},"5 adapters (Standard, V2, V2Fix, XLS, Custom)",[1157,5324,5325,5329,5332],{},[1170,5326,5327],{},[805,5328,4950],{},[1170,5330,5331],{},"Sony Music",[1170,5333,5334],{},"2 adapters (Standard, Legacy)",[1157,5336,5337,5341,5344],{},[1170,5338,5339],{},[805,5340,4946],{},[1170,5342,5343],{},"Independent",[1170,5345,5346],{},"2 adapters (Standard, Historical)",[1157,5348,5349,5354,5357],{},[1170,5350,5351],{},[805,5352,5353],{},"FUGA",[1170,5355,5356],{},"Downtown Music",[1170,5358,5359],{},"1 adapter",[1157,5361,5362,5366,5368],{},[1170,5363,5364],{},[805,5365,5046],{},[1170,5367,5343],{},[1170,5369,5334],{},[1157,5371,5372,5377,5379],{},[1170,5373,5374],{},[805,5375,5376],{},"TuneCore",[1170,5378,5046],{},[1170,5380,5359],{},[1157,5382,5383,5388,5390],{},[1170,5384,5385],{},[805,5386,5387],{},"DistroKid",[1170,5389,5343],{},[1170,5391,5359],{},[1157,5393,5394,5399,5401],{},[1170,5395,5396],{},[805,5397,5398],{},"CD Baby",[1170,5400,5356],{},[1170,5402,5359],{},[1325,5404,5406],{"id":5405},"specialized-platforms","Specialized Platforms",[1151,5408,5409,5421],{},[1154,5410,5411],{},[1157,5412,5413,5416,5419],{},[1160,5414,5415],{},"Platform",[1160,5417,5418],{},"Type",[1160,5420,2917],{},[1165,5422,5423,5436,5448,5461,5473,5486],{},[1157,5424,5425,5430,5433],{},[1170,5426,5427],{},[805,5428,5429],{},"Bandcamp",[1170,5431,5432],{},"Direct-to-fan",[1170,5434,5435],{},"Custom CSV format",[1157,5437,5438,5442,5445],{},[1170,5439,5440],{},[805,5441,5033],{},[1170,5443,5444],{},"Concert streaming",[1170,5446,5447],{},"Date embedded in filename",[1157,5449,5450,5455,5458],{},[1170,5451,5452],{},[805,5453,5454],{},"Soundcloud",[1170,5456,5457],{},"Streaming",[1170,5459,5460],{},"Multiple report types",[1157,5462,5463,5468,5470],{},[1170,5464,5465],{},[805,5466,5467],{},"YouTube Music",[1170,5469,5457],{},[1170,5471,5472],{},"Content ID integration",[1157,5474,5475,5480,5483],{},[1170,5476,5477],{},[805,5478,5479],{},"TikTok",[1170,5481,5482],{},"Social/UGC",[1170,5484,5485],{},"Emerging format",[1157,5487,5488,5493,5496],{},[1170,5489,5490],{},[805,5491,5492],{},"Meta (Facebook/Instagram)",[1170,5494,5495],{},"Social",[1170,5497,5498],{},"Rights manager exports",[1325,5500,5502],{"id":5501},"regional-distributors","Regional Distributors",[845,5504,5505,5511,5517,5523],{},[848,5506,5507,5510],{},[805,5508,5509],{},"Phonofile"," (Nordic region)",[848,5512,5513,5516],{},[805,5514,5515],{},"Zebralution"," (Germany)",[848,5518,5519,5522],{},[805,5520,5521],{},"IDOL"," (France)",[848,5524,5525,5528],{},[805,5526,5527],{},"Altafonte"," (Spain/Latin America)",[750,5530,5532],{"id":5531},"supported-file-formats","Supported File Formats",[743,5534,5535],{},"The system handles every file format encountered in the music distribution industry:",[1151,5537,5538,5553],{},[1154,5539,5540],{},[1157,5541,5542,5545,5548,5551],{},[1160,5543,5544],{},"Format",[1160,5546,5547],{},"Extension",[1160,5549,5550],{},"Use Case",[1160,5552,2917],{},[1165,5554,5555,5569,5583,5597,5611],{},[1157,5556,5557,5560,5563,5566],{},[1170,5558,5559],{},"CSV",[1170,5561,5562],{},".csv",[1170,5564,5565],{},"Most common",[1170,5567,5568],{},"UTF-8 and ISO-8859-1 encoding support",[1157,5570,5571,5574,5577,5580],{},[1170,5572,5573],{},"Excel",[1170,5575,5576],{},".xlsx",[1170,5578,5579],{},"Standard Excel",[1170,5581,5582],{},"openpyxl with read-only mode",[1157,5584,5585,5588,5591,5594],{},[1170,5586,5587],{},"Excel Binary",[1170,5589,5590],{},".xlsb",[1170,5592,5593],{},"Large files",[1170,5595,5596],{},"pyxlsb parser, common for 500K+ rows",[1157,5598,5599,5602,5605,5608],{},[1170,5600,5601],{},"Legacy Excel",[1170,5603,5604],{},".xls",[1170,5606,5607],{},"Older systems",[1170,5609,5610],{},"xlrd parser",[1157,5612,5613,5616,5619,5622],{},[1170,5614,5615],{},"TSV",[1170,5617,5618],{},".tsv",[1170,5620,5621],{},"Some APIs",[1170,5623,5624],{},"Tab-separated values",[755,5626,5628],{"className":5627},[1272,1275,762],[743,5629,5630],{},[1282,5631],{"alt":5632,"src":5633,"width":4928},"File uploader - importing distributor royalty files with status tracking","/images/blog/musicdatalab/files_uploader.webp",[755,5635,5637],{"className":5636},[1272,1275,762],[743,5638,5639],{},[1282,5640],{"alt":5641,"src":5642,"width":4928},"Example files - sample distributor reports for testing imports and demos","/images/blog/musicdatalab/example-files.webp",[1325,5644,5646],{"id":5645},"file-format-quirks-we-handle","File Format Quirks We Handle",[845,5648,5649,5655,5661,5667],{},[848,5650,5651,5654],{},[805,5652,5653],{},"XLS files containing CSV data"," - The Orchard and ADA sometimes send .xls files that are actually CSV",[848,5656,5657,5660],{},[805,5658,5659],{},"Mixed encodings"," - Automatic fallback from UTF-8 to ISO-8859-1",[848,5662,5663,5666],{},[805,5664,5665],{},"Files with metadata headers"," - Skip non-data rows (Merlin stores dates in row 1)",[848,5668,5669,5672],{},[805,5670,5671],{},"Multi-sheet workbooks"," - Automatic sheet detection",[750,5674,5676],{"id":5675},"scale-200-million-records-and-growing","Scale: 200 Million Records and Growing",[743,5678,5679],{},"Our production system currently manages:",[755,5681,5684,5688,5692,5696],{"className":5682},[758,1068,761,762,5683],"text-center",[764,5685],{"description":5686,"title":5687},"Streaming records","200M+",[764,5689],{"description":5690,"title":5691},"Active adapters","25+",[764,5693],{"description":5694,"title":5695},"File format variations","50+",[764,5697],{"description":5698,"title":5699},"New records/month","5-10M",[755,5701,5703],{"className":5702},[1272,1275,762],[743,5704,5705],{},[1282,5706],{"alt":5707,"src":5708,"width":4928},"Main dashboard - real-time overview of streams, adapters, and income by retailer and artist","/images/blog/musicdatalab/dashboard.webp",[1325,5710,5712],{"id":5711},"performance-metrics","Performance Metrics",[1151,5714,5715,5725],{},[1154,5716,5717],{},[1157,5718,5719,5722],{},[1160,5720,5721],{},"Operation",[1160,5723,5724],{},"Performance",[1165,5726,5727,5734,5746,5754],{},[1157,5728,5729,5732],{},[1170,5730,5731],{},"Single file import (500K rows)",[1170,5733,3153],{},[1157,5735,5736,5741],{},[1170,5737,5738],{},[805,5739,5740],{},"Report generation",[1170,5742,5743],{},[805,5744,5745],{},"5-15 seconds",[1157,5747,5748,5751],{},[1170,5749,5750],{},"Full-text search across catalog",[1170,5752,5753],{},"Sub-second",[1157,5755,5756,5759],{},[1170,5757,5758],{},"Dashboard aggregations",[1170,5760,5761],{},"Real-time",[743,5763,5764],{},"The ability to generate complex royalty reports across 200 million records in just a few seconds is what sets the system apart. This is achieved through:",[845,5766,5767,5773,5779,5785],{},[848,5768,5769,5772],{},[805,5770,5771],{},"Elasticsearch"," for aggregations and full-text search",[848,5774,5775,5778],{},[805,5776,5777],{},"Pre-computed rollups"," for common report dimensions",[848,5780,5781,5784],{},[805,5782,5783],{},"Chunked async processing"," during import (2000 rows per batch)",[848,5786,5787,5790],{},[805,5788,5789],{},"Celery task queues"," for parallel processing",[755,5792,5803,5807,5810],{"className":5793},[5794,5683,5795,5796,5797,5798,5799,5800,5801,5802],"my-12","py-10","px-6","rounded-xl","border","border-primary-200","dark:border-primary-800","bg-primary-50","dark:bg-primary-900/20",[1325,5804,5806],{"id":5805},"struggling-with-messy-royalty-data-from-multiple-distributors","Struggling with messy royalty data from multiple distributors?",[743,5808,5809],{},"We built this system and we can tailor it for your label. See how it works with your actual data.",[755,5811,5814],{"className":5812},[1272,1275,5813],"mt-4",[5815,5816],"u-button",{"color":5817,"icon":5818,"label":5819,"size":5820,"to":5821,"variant":5822},"primary","i-lucide-calendar","Book a Demo","lg","/contact","solid",[750,5824,5826],{"id":5825},"the-20-standard-fields","The 20 Standard Fields",[743,5828,5829],{},"After analyzing dozens of distributor formats, we defined a universal schema that captures all essential royalty data:",[1151,5831,5832,5841],{},[1154,5833,5834],{},[1157,5835,5836,5838],{},[1160,5837,2883],{},[1160,5839,5840],{},"Fields",[1165,5842,5843,5853,5863,5873,5883],{},[1157,5844,5845,5850],{},[1170,5846,5847],{},[805,5848,5849],{},"Identification",[1170,5851,5852],{},"Artist, Track, Product (album), Label, UPC, EAN, ISRC",[1157,5854,5855,5860],{},[1170,5856,5857],{},[805,5858,5859],{},"Location",[1170,5861,5862],{},"Country code (ISO), Territory name",[1157,5864,5865,5870],{},[1170,5866,5867],{},[805,5868,5869],{},"Time Period",[1170,5871,5872],{},"Year, Month, Period begin/end dates",[1157,5874,5875,5880],{},[1170,5876,5877],{},[805,5878,5879],{},"Financial",[1170,5881,5882],{},"Currency, Units (streams/downloads), Unit price, Income, Income type (GROSS/NET)",[1157,5884,5885,5889],{},[1170,5886,5887],{},[805,5888,5415],{},[1170,5890,5891],{},"Retailer (Spotify, Apple Music, etc.), Service type (streaming, download, sync)",[755,5893,5895],{"className":5894},[1272,1275,762],[743,5896,5897],{},[1282,5898],{"alt":5899,"src":5900,"width":4928},"Advanced filters - querying across all 20 standard fields with full-text search","/images/blog/musicdatalab/filters.webp",[750,5902,5904],{"id":5903},"real-world-challenges-we-solved","Real-World Challenges We Solved",[1325,5906,5908],{"id":5907},"the-ada-complexity","The ADA Complexity",[743,5910,5911,5912,5915],{},"ADA (Warner's distribution arm) required ",[805,5913,5914],{},"5 different adapters"," due to format inconsistencies:",[1151,5917,5918,5931],{},[1154,5919,5920],{},[1157,5921,5922,5925,5928],{},[1160,5923,5924],{},"#",[1160,5926,5927],{},"Adapter",[1160,5929,5930],{},"Reason",[1165,5932,5933,5943,5953,5963,5976],{},[1157,5934,5935,5937,5940],{},[1170,5936,2032],{},[1170,5938,5939],{},"Standard",[1170,5941,5942],{},"Current production format",[1157,5944,5945,5947,5950],{},[1170,5946,1970],{},[1170,5948,5949],{},"V2",[1170,5951,5952],{},"New format introduced in 2024",[1157,5954,5955,5957,5960],{},[1170,5956,2215],{},[1170,5958,5959],{},"V2Fix",[1170,5961,5962],{},"Edge case corrections for V2",[1157,5964,5965,5968,5971],{},[1170,5966,5967],{},"4",[1170,5969,5970],{},"XLS",[1170,5972,5973,5974,2749],{},"Legacy ",[1455,5975,5604],{},[1157,5977,5978,5981,5984],{},[1170,5979,5980],{},"5",[1170,5982,5983],{},"Custom",[1170,5985,5986,5988],{},[1455,5987,5604],{}," files that actually contain CSV data",[1325,5990,5992],{"id":5991},"entity-resolution","Entity Resolution",[743,5994,5995],{},"We maintain mapping databases to normalize inconsistent naming:",[1448,5997,6000],{"className":5998,"code":5999,"language":1453,"meta":887},[1451],"\"SME\"                  →  \"Sony Music Entertainment\"\n\"SPOTIFY AB\"           →  \"Spotify\"\n\"UNITED KINGDOM (GB)\"  →  \"GB\"\n",[1455,6001,5999],{"__ignoreMap":887},[1007,6003,6004],{},[743,6005,6006],{},"Entity resolution turned out to be one of the most valuable features - clean, unified naming across all platforms made reporting dramatically more useful.",[755,6008,6010],{"className":6009},[1272,1275,762],[743,6011,6012],{},[1282,6013],{"alt":6014,"src":6015,"width":4928},"Entity mapping - normalizing retailer names across distributors (TuneCore, Soundcloud, Slacker)","/images/blog/musicdatalab/mappings.webp",[1325,6017,6019],{"id":6018},"encoding-and-format-detection","Encoding and Format Detection",[743,6021,6022],{},"Every file goes through automatic format detection and encoding fallback - because \"just UTF-8\" is never the reality in international music data.",[750,6024,753],{"id":752},[755,6026,6029,6033,6036,6040,6044,6048],{"className":6027},[758,1068,6028,761,762,5683],"md:grid-cols-4",[764,6030],{"description":6031,"title":6032},"Backend","Python",[764,6034],{"description":6035,"title":5771},"Search & aggregations",[764,6037],{"description":6038,"title":6039},"Async processing","Celery + Redis",[764,6041],{"description":6042,"title":6043},"Relational storage","PostgreSQL",[764,6045],{"description":6046,"title":6047},"Containerization","Docker",[764,6049],{"description":6050,"title":6051},"Infrastructure","Digital Ocean",[750,6053,6055],{"id":6054},"results","Results",[743,6057,6058],{},"After 18 months of development:",[845,6060,6061,6067,6072,6078,6084],{},[848,6062,6063,6066],{},[805,6064,6065],{},"200 million records"," processed and searchable",[848,6068,6069,6071],{},[805,6070,5289],{}," for major music distributors",[848,6073,6074,6077],{},[805,6075,6076],{},"Report generation in 5-15 seconds"," regardless of data volume",[848,6079,6080,6083],{},[805,6081,6082],{},"Sub-second search"," across the entire catalog",[848,6085,6086,6089],{},[805,6087,6088],{},"Zero manual data entry"," - fully automated pipeline",[755,6091,6093],{"className":6092},[1272,1275,762],[743,6094,6095],{},[1282,6096],{"alt":6097,"src":6098,"width":4928},"Indexed data - browsing 200M+ streaming records with currency conversion and period tracking","/images/blog/musicdatalab/index-list.webp",[755,6100,6102],{"className":6101},[1272,1275,762],[743,6103,6104],{},[1282,6105],{"alt":6106,"src":6107,"width":4928},"Reports - royalty breakdown by artist and retailer with export to XLSX and CSV","/images/blog/musicdatalab/reports.webp",[755,6109,6111],{"className":6110},[1272,1275,762],[743,6112,6113],{},[1282,6114],{"alt":6115,"src":6116,"width":4928},"Reports - royalty breakdown by artist and product with final income","/images/blog/musicdatalab/reports-products.webp",[1325,6118,6120],{"id":6119},"development-in-numbers","Development in Numbers",[743,6122,6123],{},"Building a system of this complexity doesn't happen overnight. Here's what the development effort looked like behind the scenes:",[755,6125,6127,6131,6135,6139],{"className":6126},[758,1068,6028,761,762,5683],[764,6128],{"description":6129,"title":6130},"Commits","727",[764,6132],{"description":6133,"title":6134},"Files created","1,500+",[764,6136],{"description":6137,"title":6138},"Active development days","84",[764,6140],{"description":6141,"title":6142},"Peak commits in a single week","103",[743,6144,6145,6146,6149],{},"The development followed an intense sprint delivery model - 80% of all commits landed within a focused 3-month window (December 2024 – February 2025), averaging nearly ",[805,6147,6148],{},"9 commits per active day",". Each distributor adapter went through multiple iterations: for every new feature shipped, approximately 3 follow-up fixes were committed to handle real-world edge cases in date parsing, encoding, and field normalization.",[743,6151,6152],{},"This iterative approach - build, import real data, discover edge cases, fix, repeat - was essential. Music distribution data is messy by nature, and no amount of upfront design can replace the feedback loop of processing actual royalty files from 20+ platforms.",[1325,6154,6156],{"id":6155},"what-we-learned","What We Learned",[787,6158,6159],{},[845,6160,6161,6167,6173,6179],{},[848,6162,6163,6166],{},[805,6164,6165],{},"Start with abstraction, add workarounds later"," - The Adapter Pattern gave us a clean base, but real-world data required pragmatic hacks.",[848,6168,6169,6172],{},[805,6170,6171],{},"Date parsing is always harder than you think"," - We encountered formats we never imagined existed.",[848,6174,6175,6178],{},[805,6176,6177],{},"Entity resolution is a feature, not a bug"," - Union mapping became one of the most valuable features for reporting.",[848,6180,6181,6184],{},[805,6182,6183],{},"Scale matters from day one"," - Designing for millions of records upfront saved us from painful rewrites.",[750,6186,6188],{"id":6187},"whats-next","What's Next?",[743,6190,6191],{},"We're exploring:",[845,6193,6194,6205,6211,6217],{},[848,6195,6196,6204],{},[805,6197,6198,6203],{},[827,6199,6202],{"href":6200,"rel":6201},"https://clickhouse.com/",[831],"ClickHouse"," as the analytics engine"," - For the next project of this scale, we would replace Elasticsearch with ClickHouse for analytical queries. Its columnar storage and vectorized execution are purpose-built for aggregating billions of rows, offering significantly faster performance for royalty reporting workloads",[848,6206,6207,6210],{},[805,6208,6209],{},"ML-based column mapping"," - Automatically detect field mappings for new distributors",[848,6212,6213,6216],{},[805,6214,6215],{},"Real-time streaming ingestion"," - Move from batch files to API integrations",[848,6218,6219,6222],{},[805,6220,6221],{},"Anomaly detection"," - Flag unusual patterns in royalty data",[4447,6224,6225],{},"html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s5tWE, html code.shiki .s5tWE{--shiki-light:#E53935;--shiki-light-font-style:italic;--shiki-default:#F07178;--shiki-default-font-style:italic;--shiki-dark:#F07178;--shiki-dark-font-style:italic}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":887,"searchDepth":888,"depth":888,"links":6227},[6228,6233,6236,6241,6244,6248,6249,6254,6255,6259],{"id":4917,"depth":888,"text":4918,"children":6229},[6230,6231,6232],{"id":4960,"depth":1542,"text":4961},{"id":5060,"depth":1542,"text":5061},{"id":5067,"depth":1542,"text":5068},{"id":5077,"depth":888,"text":5078,"children":6234},[6235],{"id":5101,"depth":1542,"text":5102},{"id":5282,"depth":888,"text":5283,"children":6237},[6238,6239,6240],{"id":5293,"depth":1542,"text":5294},{"id":5405,"depth":1542,"text":5406},{"id":5501,"depth":1542,"text":5502},{"id":5531,"depth":888,"text":5532,"children":6242},[6243],{"id":5645,"depth":1542,"text":5646},{"id":5675,"depth":888,"text":5676,"children":6245},[6246,6247],{"id":5711,"depth":1542,"text":5712},{"id":5805,"depth":1542,"text":5806},{"id":5825,"depth":888,"text":5826},{"id":5903,"depth":888,"text":5904,"children":6250},[6251,6252,6253],{"id":5907,"depth":1542,"text":5908},{"id":5991,"depth":1542,"text":5992},{"id":6018,"depth":1542,"text":6019},{"id":752,"depth":888,"text":753},{"id":6054,"depth":888,"text":6055,"children":6256},[6257,6258],{"id":6119,"depth":1542,"text":6120},{"id":6155,"depth":1542,"text":6156},{"id":6187,"depth":888,"text":6188},{"name":6261,"logo":6262,"colorLogo":906},"CherryRed Records","/images/logos/cherryredrecords.webp","How we aggregated 200M streaming records from Spotify, Apple Music, YouTube and more into a unified royalty data integration system.",21,{"src":6266,"hasLogo":69},"/images/case-studies/musicdata-lab-universal-music-data-parser-case-study.webp",{"enabled":906,"items":6268},[6269,6272,6274,6276],{"text":6270,"icon":6271},"200M+ streaming records parsed from 25+ adapters covering Spotify, Apple Music, YouTube, and more.","i-lucide-database",{"text":6273,"icon":4528},"Royalty reports generated in 5-15 seconds across the entire 200M record dataset.",{"text":6275,"icon":4893},"Adding a new distributor adapter takes only 2-3 hours thanks to the Adapter Pattern.",{"text":6277,"icon":6278},"727 commits over 84 active development days with 80% shipped in a 3-month sprint.","i-lucide-git-branch",{},"/blog/case-studies/musicdata-lab-universal-music-data-parser-case-study",{"title":38,"description":6263},[897,6283],"music-data","diVoP5kprNqXIFsAqtQbRWhZovPzdtIq4MGh8CoznAA",1780305227010]