Category: General

  • Fler AI-tjänster med text-to-image

    Fler AI-tjänster med text-to-image

    År 2022 testade jag sex stycken AI-tjänster som kunde skapa en bild från text. Texten var kort och gott “Dog runs over green grass”. Jag testade StarryAI, Hotpot, Pixray, Computer Vision, DeepAI och Dream.

    Idag har jag testat tre AI-tjänster till, och deras text-to-image. Dessa är Microsoft Copilot, Google Gemini och OpenAI ChatGPT. För detta test använder jag endast gratisversioner som är tillgängliga som online-tjänst gratis. (Åtminstone gratis om man ser till pengatransaktioner.) Jag har inte följt upp Pixray, Computer Vision eller Dream.

    År 2022 kunde man inte ha åsikter om bildens stil, vilket går utmärkt idag. Jag valde att lämna bildens stil helt okommenterad, för att se vad jag får på samma gamla prompt idag.

    Detta fick jag av StarryAI för tre år sedan, när jag bad om “Dog runs over green grass.”:

    Idag gav samma prompt denna bild från StarryAI:

    Hotpot kändes ganska i framkant år 2022 och gav mig denna bild:

    Med exakt samma prompt idag, fick jag detta från Hotpot:

    Detta fick jag från DeepAI år 2022:

    Det är oklart vad på bilden som föreställer en hund och vad på bilden som föreställer gräs. Detta fick jag från DeepAI idag:

    Copilots, Geminis och ChatGPTs text-to-image-funktioner är placerade bakom en språkmodell, så man måste berätta för språkmodellen att det är text-to-image man är intresserad av. Därför har jag uppdaterat prompten till följande:

    Can you give me an image? Dog runs over green grass.

    Igen, ingen kommentar om stil eller kontext. Jag vill se vad de hostar fram. Detta kommer från Copilot:

    Gemini:

    ChatGPT:

    Bilderna kan såklart inte mäta sig med fotografier (än), åtminstone inte när man tittar på gratis-upplagorna, men denna förändring säger mig en sak: En teknik som fortfarande ligger i sin vagga, har förbättrats i väldigt hög hastighet under några få år. Om detta fortsätter i några år till, kommer världen att se väldigt annorlunda ut.

  • Realtidsrendering av mandelbrotmängden

    Här är en realtidsrendering av mandelbrotsmängden skriven i HTML5/JS. Jag har själv en ganska slö dator, så jag ökar aldrig antalet iterationer till mer än 800, men det är lätt att korrigera. Zooma med mushjulet, panorera genom att klicka och dra. Så här ser källkoden ut:

    
    <!DOCTYPE html>
    <html lang="sv">
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta charset="utf-8">
        <title>Mandelbrot med zoom och panorering</title>
        <style>
            html, body {
                height: 100%;
                margin: 0;
                padding: 0;
            }
    
            body {
                display: flex;
                justify-content: center;
                -ms-align-items: center;
                -o-align-items: center;
                -webkit-align-items: center;
                align-items: center;
                background: #0000aa;
            }
    
            canvas {
                border: 1px solid #4466ff;
                display: block;
                margin: auto;
                background: #000000;
                cursor: grab;
            }
    
            canvas:active {
                cursor: grabbing;
            }
        </style>
    </head>
    <body>
        <canvas id="canvas" width="640" height="400"></canvas>
        <script>
            const canvas = document.getElementById("canvas");
            const ctx = canvas.getContext("2d");
            let width = canvas.width;
            let height = canvas.height;
            let xMin = -2.5, xMax = 1;
            let yMin = -1.25, yMax = 1.25;
            var maxIterations = 50;
            let isDragging = false;
            let dragStartX, dragStartY;
            let dragStartXmin, dragStartXmax, dragStartYmin, dragStartYmax;
    
            function drawMandelbrot() {
                const img = ctx.createImageData(width, height);
                const data = img.data;
    
                for (let py = 0; py < height; py++) {
                    const y0 = yMin + (py / height) * (yMax - yMin);
    
                    for (let px = 0; px < width; px++) {
                        const x0 = xMin + (px / width) * (xMax - xMin);
                        let x = 0, y = 0, iteration = 0;
    
                        while (x * x + y * y <= 4 && iteration < maxIterations) {
                            const xTemp = x * x - y * y + x0;
                            y = 2 * x * y + y0;
                            x = xTemp;
                            iteration++;
                        }
    
                        const color = iteration === maxIterations ? 0 : 255 - Math.floor(iteration * 255 / maxIterations);
                        const index = 4 * (py * width + px);
                        data[index] = color;
                        data[index + 1] = color;
                        data[index + 2] = color;
                        data[index + 3] = 255;
                    }
                }
    
                ctx.putImageData(img, 0, 0);
            }
    
            drawMandelbrot();
    
            canvas.addEventListener("wheel", function (event) {
                event.preventDefault();
                const mouseX = event.offsetX;
                const mouseY = event.offsetY;
                const zoomFactor = event.deltaY < 0 ? 0.9 : 1.1;
    
                if (zoomFactor < 1.0) {
                    maxIterations += 2;
                } else {
                    maxIterations -= 2;
                }
    
                if (maxIterations < 30) {
                    maxIterations = 30;
                }
                else if (maxIterations > 800) {
                    maxIterations = 800
                }
    
                const xCenter = xMin + (mouseX / width) * (xMax - xMin);
                const yCenter = yMin + (mouseY / height) * (yMax - yMin);
                const newWidth = (xMax - xMin) * zoomFactor;
                const newHeight = (yMax - yMin) * zoomFactor;
                xMin = xCenter - (mouseX / width) * newWidth;
                xMax = xMin + newWidth;
                yMin = yCenter - (mouseY / height) * newHeight;
                yMax = yMin + newHeight;
                drawMandelbrot();
            });
    
            canvas.addEventListener("mousedown", function (event) {
                isDragging = true;
                dragStartX = event.offsetX;
                dragStartY = event.offsetY;
                dragStartXmin = xMin;
                dragStartXmax = xMax;
                dragStartYmin = yMin;
                dragStartYmax = yMax;
            });
    
            canvas.addEventListener("mousemove", function (event) {
                if (!isDragging)
                    return;
    
                const dx = event.offsetX - dragStartX;
                const dy = event.offsetY - dragStartY;
                const xShift = (dx / width) * (dragStartXmax - dragStartXmin);
                const yShift = (dy / height) * (dragStartYmax - dragStartYmin);
                xMin = dragStartXmin - xShift;
                xMax = dragStartXmax - xShift;
                yMin = dragStartYmin - yShift;
                yMax = dragStartYmax - yShift;
                drawMandelbrot();
            });
    
            canvas.addEventListener("mouseup", () => { isDragging = false; });
            canvas.addEventListener("mouseleave", () => { isDragging = false; });
        </script>
    </body>
    </html>
  • Sprdef2 1.1 is available

    The C64/128 sprite editor Sprdef2 (runs under Windows) is now available for download. Screenshot and link is available here: https://github.com/Anders-H/Sprdef2/blob/main/README.md

  • JobRunner 1.8 (preview)

    A preview version of JobRunner 1.8 can now be downloaded. The most important addition is conditional execution that now can be specified when a new job is created.

    Website: https://github.com/Anders-H/JobRunner/blob/master/README.md

  • Secret Agent Man – teaser 1

    Nu finns första teaser-videon till spelet Secret Agent Man att betrakta i all sin glans!

    Hemsidan finns här: https://secretagentman.80tal.se/

  • 2025-versionen av Snövit kan vara ett misstag

    Nu har The Walt Disney Company (Disney) släppt en nyinspelning av klassikern Snövit och de sju dvärgarna (1937), kort och gott kallad Snövit. Från början skulle det vara en “live action movie”, men Disney ändrade sig i sista sekund och valde att klippa in skådespelare (främst Rachel Zegler och Gal Gadot) i en datoranimerad film, som ett slags omvänd “Vem satte dit Roger Rabbit” (1988).

    Disneys film från 1937 innehåller en extremt problematisk sekvens. Rollfiguren Snövit tar sig in i ett hem som inte tillhör henne och använder dess faciliteter. Som betalning städar hon hemmet. Hemmet visar sig tillhöra sju manliga gruvarbetare. Hur kan man göra en nyinspelning av en film som innehåller ett moment av en kvinna som frivilligt städar? Åt män, dessutom? Givetvis anpassade Disney filmen så att Snövit betalar för gästfriheten genom att låta männen städa efter sig själva – en mer eller mindre nödvändig förändring för att vinna acceptans år 2025.

    Något som är mer intressant är att Zegler, som spelar Snövit, privat har tagit ställning för Palestinarörelsen, som i sin tur stöttar Hamas och därmed inte bara allmän högerextremism, utan specifikt Hitlers rendering av Nazismen. Det är inte särskilt kontroversiellt idag när alla älskar Palestina lika mycket som de hatar judar, men eftersom Gal Gadot är jude (och därmed har vissa invändningar mot nazismen) så fungerar inte personkemin särskilt bra. Till detta, en personlig kommentar är att musiken var dålig, att Snövit inte var särskilt vacker och att den ondskefulla drottningen är vackrare än Snövit.

    Om Disney får tillbaka de tre miljarder de spenderade, så säger det verkligen något om hur starkt deras varumärke är. I skrivande stund är IMDb-betyget på Snövit hela 2,4/10, men det när nyhetens behag har lagt sig, brukar det sjunka en hel del.

  • AI kommer aldrig kunna…

    Artificiell intelligens (AI) har varit med i matchen längre än mig. Under senare år har både användbarheten och antalet tillämpningar ökat. I mitten på 1980-talet blev AI tillgängligt för hemdatorer. Keith Brain och Steven Brain skrev boken “Artificial intelligens on the…” följt av olika plattformars namn, som t.ex. Commodore 64, Sinclair ZX Spectrum, Sinclair QL, BBC och Electron, med undertiteln “make your micro think”. Ordet “micro” är en förkortning av en mikrodator, och en mikrodator är en dator som hade en “mikroprocessor“, som t.ex. Commodore 64. Boken berörde mest förståelsen av naturligt språk och mönsterigenkänning. Idag kan datorerna göra mycket mer.

    Under resans gång har vi sett dessa kapaciteter kompletteras med förutsägelser baserat på avancerad statistik, tal-till-text, bildigenkänning, bildgenerering och stora språkmodeller.

    Varje nytt genombrott som rapporterats, har ifrågasatts av personer vars kunskap på ämnet liknar en överkörd grävlings insatser i stavhopp. Schack skulle vara den slutgiltiga skillnaden mellan människa och maskin, men så var det inte. Så snart man fått datamaskiner att fatta självständiga beslut, deklarerade en socionom från Malmö att datorer endast kan göra det man säger åt dem. Så snart man fått datamaskiner att generera illustrationer till text, deklarerade en kommunal kommunikatör från Göteborg att datorer inte kan ersätta människan där heller.

    Något så enkelt som att AI förutspådde graden av uppsägningar i samband med pandemin år 2019, och att AI hittade slöseri av kommunala pengar upp till 20% av kommunernas budget, ändrar inte folks uppfattning, för ingen uttalar sig så självsäkert i ett ämne som de som inte vet något om ämnet.

    “AI kan inte ditt”, “AI kommer aldrig kunna datt”. Men den relevanta frågan borde vara: Hur vet du det?

    Och varför delar inte personer med ämneskompetens din uppfattning?

    Fenomenet kallas för Dunning-Kruger-effekten. Den som inte har kunskap också är oförmögen att förstå att han eller hon överskattar sin kunskap i ett ämne. Därför kommer dessa deklarationer alltid från personer som inte vet vad AI är.

    När det gäller AI är den drivande effekten ett slags uppfattning om att det man vill ska vara sant, uppfattas som sant. Den som inte vill att AI ska kunna utföra vissa arbetsuppgifter som idag utförs av människor, anser att så inte är fallet. Och för att skydda den uppfattningen, undviker man ny kunskap. Men man undviker definitivt inte “expertutlåtanden” i sakfrågan!

    Anledningen till att holdingbolaget Sony Music som äger t.ex. Queens musik, gärna ser att människor lyssnar på och studerar Queen, men vill förbjuda robotar att göra detsamma, är naturligtvis för att de vet att det idag finns AI som kan åstadkomma så mycket mer än vad en människa kan göra, och att de då själva vill vara den som har en AI som är tränad på Queen, så att de kan vara ensamma om att ge ut ny Queen-musik. Detta sker samtidigt som vanligt folk anser att AI inte bara är obrukbart inom kulturen idag, utan att den aldrig kommer vara användbar.

  • En populär lösning på ett icke-existerande problem

    Om du har dragit en strömkabel från en punkt till en annan, men råkat runda en regel så snäv att inte strömkabelns ändar kan göra samma runda, så har du förmodligen ett problem.

    Det råder tydligen ingen fara! Du kan lösa detta utan att klippa av någon av strömkabelns ändar! Men varför drar någon en kabel på detta vis? Det är till och med så att en video som ger en lösning på problemet, ifrågasätter problemet!

    Troligen vill man dra en sladd under det trånga utrymmet, för att få lite ordning och reda, och det kan vara svårt att komma loss ifrån. Men lösningen som alltid presenteras hanterar något helt annat: När någon pillat in en ögla av sladden under ett trångt utrymme, och dragit kontakten genom den öglan. Detta är något man väldigt sällan gör, men har man gjort det, så behöver man inte klippa av sladden för att få bort den. De tvåpoliga eurokontakterna är enklare att dra, för de har ingen fet kontakt i änden, men jag har faktiskt en och annan sladdskarvdosa installerat, för att jag helt enkelt klippt av en sladd för att spara jobb.

  • Demokraterna sprider desinformation om “molnet”

    Partiet Demokraternas presidentkandidat Kamala Harris gjorde nyligen ett uttalande om hur s.k. cloud computing fungerar. Min transkribering:

    Eh, so, you, no longer are you necessarily keeping those private files in some file cabinet that is locked in the basement of the house. It’s on your laptop and it’s then therefore up here in this cloud, that exists above us. Right? It’s no longer in a physical place!

    Min fria tolkning är att Internet innan cloud computing innebar serverlagring att lagringen skedde i din egen källare, men att med cloud computing så behöver inte filerna längre någon fysisk plats, och att källaren därmed kunde bli en gillestuga.

    Detta är inte helt korrekt.

    Cloud syftar inte till vatten i gasform, utan till flera webbservrar. I stället för att en webbadress tar dig till en server som håller reda på ditt sessionsdata, så tar dig webbadressen till en tjänst som levereras av (potentiellt) många servrar.

    Nackdelen med detta är att sessionsdata måste skickas fram och tillbaka mellan klient och server, eftersom en array av servrar står redo att hantera inkommande förfrågningar, där den ene inte nödvändigtvis vet vad den andre gör. Fördelen är att upp- och nedskalning kan göras mer dynamiskt, med kortare (i princip inga) ledtider, inom denna array.

    Att något lagras i molnet, säger bara något om hur det lagras, inte något om datats fysiska plats. Cloud computing löser i princip slashdot-problemet, inte problemet med att serverlagring kräver en serverhall i källaren – det problemet löste själva Internet.

  • JobRunner 1.7

    JobRunner is a Windows program that executes tasks sequentially. Typically it is used for client rendered web sites. Version 1.7 of JobRunner lets you set retry count for individual jobs. Future updates can be found here. Download installation program here (right click, save as…). Source code is available on Github.

  • Nedtrappningen under sommaren

    Jag har varit lite frånvarande under en period, men det är inget värre än att jag bara har svårt att få tiden att räcka till med allt som pågår. Medan jag jobbar är jag alltid närvarande på Twitter/X, och då och då skickar jag upp en lite kortare video på YouTube. Jag poddar fortfarande med Henrik Andersson. Här hittar du vår musikpodcast och här hittar du våra filmrecensioner (som vi filmar). Jag kommer att fortsätta att vara lite frånvarande i ytterligare några veckor, men därefter kan ni räkna med nya inlägg här.

    Så länge, tänk gärna på Figgehn, Blågula bilen, Humanisthjälpen och Piratpartiet.

  • Resurser för Commodore 64-utveckling

    Om du som jag skriver program för Commodore 64 på fritiden, är denna sida ett register över de verktyg du behöver.

    Utvecklingsmiljöer

    CBM .prg Studio

    C64 Studio

    Programmeringsspråk

    Turbo Rascal

    KickC

    XC=BASIC 3

    Vision BASIC

    Commodore DOS

    DirMaster

    Dokumentation

    BASIC (C64 Wiki)

    Assembler (C64 Wiki)

    All about yout C64 – online help

    Chip music for dummies

    Commodore 64 memory map

    Machine language

    6402 opcodes

    Programming sound and music on yout Commodore 64

    Final Cartridge III manual

    VIC-1541 manual

    Commodore 64 Data Files

    Commodore BASIC tokenized file

    Editorer, musik

    GoatTracker 2

    SID Factory II

    Music Studio

    Editorer, grafik

    mcDRAW

    PETSCII Editor

    Emulering

    VICE

    Övrigt

    Awesome Commodore Resources

    The C-64 Scene Database

    För min del har jag skrivit bok om Commodore BASIC 2.0 second release, och jag håller på att arbeta med en sprite editor.

  • En programmerare programmerar

    Jag är inte helt säker på att programmerare är ett framtidsyrke, för behovet dras allt jämt åt kring ett minskande behov av ytterligare ett whateverramverk för dagens systemutvecklare att använda. Som jag har uppfattat marknadens behov så kan det vara lönsamt för generalister att vara så generella som möjligt. En full stack-utvecklare bör vara så bred som möjligt, för att vara relevant. En specialist bör vara så smal som möjligt för att vara relevant. Jag kan tycka att en arbetsvecka är rejält utmattande, men faller gärna tillbaka till något som jag själv finner vara intellektuellt stimulerande, som t.ex. den Altair-emulator som jag och den mycket skärpte Tomas Håkansson har arbetat med i ett par år nu. Det lär dröja ytterligare ett par år innan version 1.0 finns på plats, men jag ser verkligen fram emot det.

  • Kvällens nästanrymning

    Vi bor mellan två rullstensåsar. Till väster ligger åsen med vägen norrut mot Örebro, och till öster ligger åsen med stallet och hagen. Kvällen började med att ett fallet träd hade krossat staketet och att vi noterade att flocken som stod där, endast bestod av 4 hästar – det nya stoet saknades. Efter att ha lagat skadan och letat igenom skogen utanför, hittade vi den sista. Så hon verkar trivas sitt nya stall.

  • Häst #5 är installerad

    Nu har vi installerat den femte hästen, ett sto (svensk varmblod) vid namn Akasia. Så kvällen kommer att ägnas åt att etablera den nya rangordningen i flocken. Min dotter Lova tog bilden.

  • JobRunner 1.6 is available

    Version 1.6 of JobRunner is available. It allows an system administrator to disable individual jobs in a batch.

    Download installation program (Windows 10 or later): JobRunner.exe (Right click, Save link as…)

    Source code (C# 8, .NET Framework 4.8): https://github.com/Anders-H/JobRunner

  • Några knep för att få fart på Commodore BASIC i C64

    Commodore BASIC 2.0 på Commodore 64 har några kända prestandaproblem, och denna enkla kod har implementerat några stycken:

    10 S=TI
    20 X=0.01
    30 FOR A=0 TO 255
    40 REM
    50 REM
    60 REM
    70 X=X+0.01
    80 POKE 16384,A
    90 REM
    100 REM
    110 REM
    120 NEXT
    130 S=TI-S
    140 PRINT "TIME" S
    150 PRINT "RESULT " X

    Programmet startar en tidtagning och 256 gånger görs en flyttalsberäkning och ett heltal skrivs till en minnesadress (16384). Programmet tar 327 jiffies i tid. (En jiffy är ungefär 1/60 sekund.)

    Att hitta ett radnummer tar tid, så om man optimerar genom att radera alla REM-satser, blir exekveringstiden istället 299 jiffies.

    10 S=TI
    20 X=0.01
    30 FOR A=0 TO 255
    70 X=X+0.01
    80 POKE 16384,A
    120 NEXT
    130 S=TI-S
    140 PRINT "TIME" S
    150 PRINT "RESULT " X

    Om man dessutom lägger alla radnummer tätt ihop, försvinner faktiskt en jiffie till.

    0 S=TI
    1 X=0.01
    2 FOR A=0 TO 255
    3 X=X+0.01
    4 POKE 16384,A
    5 NEXT
    6 S=TI-S
    7 PRINT "TIME" S
    8 PRINT "RESULT " X

    Att tolka ett heltal tar tid, så genom att skapa en variabel innehållande adressen 16384 behöver tolken bara göra det arbetet en gång.

    0 S=TI
    1 X=0.01
    2 N=16384
    3 FOR A=0 TO 255
    4 X=X+0.01
    5 POKE N,A
    6 NEXT
    7 S=TI-S
    8 PRINT "TIME" S
    9 PRINT "RESULT " X

    Detta skalar bort ytterligare 81 jiffies, så att programmet nu inte tar mer än 217 jiffies. Den sista optimeringen vi kan göra, är bara lämplig när arbetet som utförs inte kräver att bildskärmen är aktiv. Genom att stänga av bildskärmen, försvinner en tidskrävande arbetsuppgift, vilket gör att processorn kan lägga mer fokus på BASIC-programmet. På adress 53265 ligger normalt värdet 27, vilket betyder att skärmen ska vara aktiv. Genom att sätta 11 på adressen, stänger man av skärmen, när arbetsuppgiften är klar, kan man aktivera skärmen igen genom att återställa värdet till 27.

    0 POKE 53265,11
    1 S=TI
    2 X=0.01
    3 N=16384
    4 FOR A=0 TO 255
    5 X=X+0.01
    6 POKE N,A
    7 NEXT
    8 S=TI-S
    9 PRINT "TIME" S
    10 PRINT "RESULT " X
    11 POKE 53265,27

    Vi är nu nere på 205 jiffies för samma arbetsuppgift, vilket är 63% av ursprungstiden på 327 jiffies. BASIC är fortfarande lite för slött för att vara användbart när datorspel ska byggas, men om man väljer BASIC i kommersiell mjukvara, är det viktigt att göra rätt.

    De delar av Pirates! som inte kräver realtidsexekvering är faktiskt skrivna i BASIC, och även spelet Artworx Strip Poker var skrivet i BASIC.

  • En Windows-komponent för Commodore-sprites

    En Windows Forms-komponent som kan presentera en Commodore 64-sprite är kanske inte vad man använder varje dag. Men jag har byggt en sådan (.NET Framework 4.8) för att isolera många tekniska bekymmer i den sprite-editor för C64 jag arbetar med. Om du vill leka med den, så finns den här:

    Install-Package EditStateSprite

    Paketet innehåller en GUI-komponent för att redigera pixeldata, en dialog för att modifiera färgpaletten, diverse funktioner för att manipulera en sprite samt funktioner för serialisering och deserialisering. Paketet uppdateras ganska ofta, eftersom jag ständigt hittar nya behov i sprite-editorn jag arbetar med. För att komma i gång:

    _sprites.Add(new SpriteRoot(false));
    spriteEditorControl1.ConnectSprite(_sprites[0]);

    Som exempel, om du vill spegelvända en sprite:

    spriteEditorControl1.Flip(TwoWayDirection.LeftRight);
    Invalidate();

    Om du vill låta användaren ändra färgpaletten:

    spriteEditorControl1.PickPaletteColors(this);
    Invalidate();

    Eller kanske läsa in ett sedan tidigare sparat dokument med C64-sprites:

    _sprites.Load(@"D:\Temp\sprites.sprdef");
    spriteEditorControl1.ConnectSprite(_sprites[0]);
    Invalidate();

    Ett fungerande exempel finns att titta på här (TestProgram). Mycket nöje!

  • Vilket programmeringsspråk är enkelt?

    Frågan om vilka programmeringsspråk som är enkla brukar dyka upp från nybakade programmerare. Det är inte jättelätt att besvara frågan, för det finns så många aspekter som påverkar komplexiteten i ett programmeringsspråk. Är språket enkelt för att språket går att lära sig på kort tid? I så fall är C ett utmärkt enkelt språk. Du måste veta om att du ska skapa en entry point (alltså skriva int main()) och du måste förstå #include, men därefter är det bara att gasa på. Det totalt tre uppdelade i tre kategorier: Flödeskontroll, input/output och beräkningar.

    För flödeskontroll finns beslut (branschtermen är branching), iterationer (kallas ofta för loopar) och hopp. Input/output handlar om kommunikation med omvärlden, som t.ex. skärm, tangentbord eller printer (printf, fread, fclose). För beräkningar finns ett antal operatorer som +, -, * och / (med flera).

    Rent objektivt finns det inte särskilt mycket att lära sig kring språket C. Helt oavsett vilket programmeringsspråk man pratar om, så måste man lära sig de koncept som är kopplade till programmering – på den punkten kan inte ett språks enkelhet hjälpa till – men när man väl kan det, så är C definitivt enkelt.

    Ju enklare språket blir, desto enklare blir det att programmera. C erbjuder 32 nyckelord och en drös med operatorer, vilket man kan lära sig på en dag. Men hur omsätter man den kunskapen till att bygga t.ex. Doom? Eller Heroes of Might and Magic? Eller ens Notepad? Man måste veta hur man använder den specifika plattform man programmerar för, vilket verkligen är en utmaning i sig!

    C# är ett språk som betraktas som svårt att lära sig. Antalet instruktioner och nyckelord är enormt, antalet metoder som finns tillgängliga för att lösa en uppgift är stort. Om du som BASIC-programmerare har en vektor innehållande fem textsträngar som du ska skriva ut på skärmen så finns det typ en enda lösning, som ser ut så här:

    FOR A=1 TO 5:PRINT S(A):NEXT A

    I C# skulle tre olika lösningar på samma problem kunna se ut så här:

    for (int i = 0; i < s.Count; i++) Console.WriteLine(s[i]);
    
    foreach (var x in s) Console.WriteLine(x);
    
    s.ForEach(Console.WriteLine);
    

    Men samtidigt, om man lär sig alla instruktioner och alla kommandon (vilket är svårt) så vet du vad som krävs för att bygga Notepad. Eller Doom. Eller rent av Heroes of Might and Magic. Det råder inga tvivel om att svärdet är tveeggat – antingen är språket enkelt men svårt att åstadkomma något i, eller så är språket komplicerat med enkelt att åstadkomma något i när man väl kan det.

    Titta gärna på skillnaden mellan Commodore BASIC och 6502 ASM för att skriva HELLO WORLD på skärmen. I BASIC blir det PRINT "HELLO WORLD":

  • ChatGPT kan lyssna på podcasts åt dig

    Med tanke på hur snabbt utvecklingen går, kommer denna text snart vara inaktuell eller felaktig. Men jag har upptäckt att jag kan konsumera enorma mängder information på väldigt kort tid genom att låta ChatGPT lyssna på poddar åt mig.

    Vill du själv testa behöver du ha tillgång till Office 365 och ett konto som låter dig använda ChatGPT Plus.

    1. Ladda hem den podcast du är nyfiken på. Spara den som fil.

    2. Starta Word i Office 365. Klicka transkribera och välj den podcast du nyss laddade hem. Spara texten i PDF-format och placera dokumentet på en webbserver. Har du ingen egen, kan du använda https://askyourpdf.com/upload. Dokumentet hamnar på askyoutpdf.com ändå när du använder det från ChatGPT.

    Om du har ett pluskonto för ChatGPT kan du köra version 4.0. När versionen är vald, kan du gå in i inställningarna och aktivera stöd för tilläggsprogram (plugins). Kontrollera att detta är gjort. Därefter, håll musen över GPT-4 (i mitten längst upp i ChatGPT) och klicka Plugins. Du får godkänna ett avtal, och därefter kan du välja vilken plugin du vill använda. Ser du redan texten No plugins enabled kan du klicka där för att få välja plugins. Installera den som heter AskYourPDF. Du bör nu se en gul symbol som indikerar att denna är vald.

    3. Ställ en fråga. Se till att det framgår vilket dokument du frågar om. I sin allra enklaste form kan du fråga om en summering. Exempelvis:

    Vad handlar den här texten om? [Korrekt länk till dokumentet]

    Så här kan ett svar se ut:

    Sen kan man fortsätta att analysera dokumentet. T.ex. fråga om det finns några självmotsägelser i texten, och efter några sekunder listas påståenden som kan tolkas som felaktiga eller självmotsägande. Och så vidare.

    Genom att arbeta på detta sätt kan du ha fått till dig innehållet i en 40 minuter lång podcast på bara en eller ett par minuter, och dessutom fått eventuella felaktigheter i det som sägs utpekade för dig.

  • A JS implementation of the Harter-Heighway Dragon Curve

    Five years ago, I did a post in Swedish on the Harter-Heighway Dragon Curve, with C# source code. I have translated that code to JavaScript and added animation to the rendering. The fractal describes a way of folding a piece of paper so that its outline takes up a surface. Anyone who has read the book Jurassic Park has probably seen the fractal depicted.

    The repository is located here and a live demo is available here.

  • How to fix the endless explorer.exe crash-and-restart loop in Windows 11

    After updating Windows 11 to 22621.1413, it got stuck in an endless loop after startup. The desktop environment, explorer.exe, crashed and restarted continuously. This can happen if you are running add-ons on top of explorer.exe, and in my case, I had ExplorerPatcher installed.

    When this happens, press Ctrl+Alt+Delete and start Task Manager. Find explorer.exe and terminate it. It is not easy because the list flickers, and you need to send the command when the program is actually running. This will stop the flickering of the screen and the system will be free to do other tasks than restarting explorer.exe.

    At this point, you cannot run modern UI applications like your computer settings, but you can run classic apps that use the native Windows GUI, like the old control panel. To get the old app for uninstalling applications, run appwiz.cpl. From there you can uninstall whatever extension that causes explorer.exe to crash, in my case ExplorerPatcher.

    Now I have to wait for this problem to be fixed before I can install it again. ExplorerPatcher is a program that I really appreciate.

  • Inverterat läge på VIC-20

    VIC-20 har en gemensam minnesadress som håller reda både på skärmens bakgrundsfärg och borderfärg. Dessutom finns en bit som håller reda på om inverterat läge används. Adressen är 36879, och de åtta bitarna på adressen beskriver borderfärg (bit 0-2), inverterat läge (bit 3) och bakgrundsfärg (bit 4-7). Inverterat läge är avstängt om bit 3 är satt till 1 (normalt läge) och aktiverat om bit 3 är satt till 0.

    Då endast tre bitar (bit 0-2) har avsatts för borderfärg, kan endast en av datorns åtta första färger väljas som borderfärg. Av någon anledning prioriterade man att få in flaggan för inverterat läge framför möjligheten att välja en av alla sexton tillgängliga färger till bordern.

    Från början är inverterat läge avstängd, vilket kan konstateras om man läser av bit 3 på adress 36879.

    PRINT PEEK(36879) AND 8

    Svaret 8 betyder att bit 3 är satt. Ursprungsvärdet på adressen är 27, vilket motsvarar 00011011. Bit 0-2 (numreras från vänster till höger) är 011 vilket motsvarar 3 (turkos). Bit 3 är 1 (normalt läge) och bit 4-7 är 0001 (vit).

    Genom att sätta bit 3 till 0 aktiveras inverterat läge.

    POKE 36879,PEEK(36879) AND 247

    Detta släcker bit 3, vilket gör att värdet på adress 36879 nu är 19 (00010011) om inget annat är ändrat. Borderfärgen är fortfarande turkos, bakgrundsfärgen är fortfarande vit, men textfärgen (blå om inget är ändrat) ligger nu bakom texten som visas med bakgrundsfärgen (vit). Det innebär att textmarkören inte syns mot vit botten, då den nu visas som vit.

    För att återställa, sätt bit 3 till 1 igen.

    POKE 36879,PEEK(36879) OR 8

  • JobRunner 1.5

    JobRunner 1.5

    JobRunner är ett program som exekverar sekvenser av program. Det kan t.ex. användas av den som regelbundet genererar filer lokalt på datorn, som ska skickas upp till en server. I version 1.5 finns stöd i användargränssnittet för att arbeta med variabler. Om du vill modifiera vilka steg som ska exekveras, måste JobRunner startas som administratör. Mer information finns här.


    Ladda hem JobRunner för Windows 10/11: JobRunner.exe (högerklicka, välj Spara som…)

  • Disabling loading turbo to make C64 programs work on the TheC64

    Disabling loading turbo to make C64 programs work on the TheC64

    On a TheC64, you would normally load a program from a disk image rather than a physical disk. If the program consists of a single file, everything is usually fine. But if several files make up the program you want to load, a custom loader is usually involved, and the built in TheC64 turbo will make the program unloadable.


    TheC64 solves this problem by looking at the ending characters in the name of the active disk image. The first disk image in my copy of the Comaland 100% demo is not called image1.d64 but image1_adro.d64. The presence of the characters “ad” tells the TheC64 to inactivate the built-in disk turbo, that makes most multipart software fail during load. The presence of the characters “ro” tells TheC64 that the disk is read-only, which makes most abandonedware work.


    The manual describes all of the character combinations that the TheC64 understand. You can control joystick ports, among other things. As a TheC64 user, you should make it a habit to add “ad” before the file ending in the image that holds your programs, and “_adro” to the end of any image you download from the C-64 Scene Database.

  • The new self-contained Hand class

    The new self-contained Hand class

    I have added a new class to my poker formation checker (now on .NET 6.0, not 5.0). The library still mainly works with string representations of decks, hands and cards, and the Hand class can be initialized from a string.

    // Create a hand from a string:
    using Winsoft.Gaming.GenericPokerFormationChecker;
    const string handString = "FORMATION=3-OF-A-KIND,SCORE=0333,HAND=CLB04-DMN06-HRTKN*-DMNKN*-SPDKN*";
    var hand = new Hand();
    hand.Parse(handString);
    Console.WriteLine(hand.Formation); // Output: ThreeOfAKind

    But the Hand class can also be initialized using regular function calls.

    // Create a hand using the API:
    using Winsoft.Gaming.GenericPokerFormationChecker;
    var hand = new Hand();
    hand.Add(Suit.Spades, Value.Ace);
    hand.Add(Suit.Clubs, Value.Ace);
    hand.Add(Suit.Diamonds, Value.Ace);
    hand.Add(Suit.Hearts, Value.Ace);
    hand.Add(Suit.Spades, Value.Value06);
    Console.WriteLine(hand.Formation); // FourOfAKind

    Also, the Hand class is self contained. You can any time prompt what formation and score it represents, and what cards are included in the formation. If the hand doesn’t have a formation, the InFormation property will point out the highest card.

    // Create a hand using the API:
    using Winsoft.Gaming.GenericPokerFormationChecker;
    var hand = new Hand();
    hand.Add(Suit.Hearts, Value.Value10);
    hand.Add(Suit.Clubs, Value.Queen);
    hand.Add(Suit.Spades, Value.Ace);
    hand.Add(Suit.Diamonds, Value.Value03);
    hand.Add(Suit.Spades, Value.Value06);
    Console.WriteLine(hand[0]!.InFormation); // false
    Console.WriteLine(hand[1]!.InFormation); // false
    Console.WriteLine(hand[2]!.InFormation); // true
    Console.WriteLine(hand[3]!.InFormation); // false
    Console.WriteLine(hand[4]!.InFormation); // false

    The old API has not changed. Install:

    Install-Package PokerFormationChecker

    Source code is here, NuGet package is here.

  • Rita en cirkel på Commodore 64

    Rita en cirkel på Commodore 64

    Det finns ett meme som visar hur omständligt det är att programmera BASIC på Commodore 64 (C64). Bilden visar den kod som krävs för att rita en cirkel på skärmen på en C64, jämfört med den kod som krävs för att rita en cirkel på skärmen på Sinclair Spectrum.

    Om du skummar igenom koden i bilderna, så noterar du säkert två saker: C64-versionen är onödigt pratig och Spectrum-versionen lider lite av att skärmupplösningen är aningen lägre än C64-versionen (256*192 pixlar jämfört med 320*200 pixlar).

    Denna kod är galet långsam, men det är det kortaste jag har lyckats åstadkomma för att rita en cirkel på C64:

    10 POKE 56,32:POKE 52,32
    20 CLR:POKE 53272,25
    30 POKE 53265,PEEK(53265) OR 32
    40 PRINT "{CLEAR}"
    50 POKE 88,0:POKE 89,63
    60 POKE 113,64:POKE 114,31
    70 POKE 12,1:SYS 45760
    80 FOR A=1 TO 7.3 STEP 0.01
    90 X=COS(A)*20
    100 Y=SIN(A)*22
    110 XX=159+4*X:YY=99+3*Y
    120 ROW=INT(YY/8):CH=INT(XX/8)
    130 LINE=YY AND 7:BIT=7-(XX AND 7)
    140 BYTE=8192+ROW*320+CH*8+LINE
    150 POKE BYTE,PEEK(BYTE) OR 2^BIT
    160 NEXT
    170 GOTO 170

    Detta är resultatet:

    Jag skulle tro att orsaken till denna skillnad är att C64 egentligen är en spelkonsol, utklädd till dator. C64 var vid lansering utrustad med extra allt inom ljud och grafik, och för att spara minne slängde man in en äldre version av Commodore BASIC, nämligen 2.0 (second release). Samma som VIC-20, men ett par versioner lägre än den PET. Den enda anledningen till att C64 faktiskt hade en BASIC och ett tangentbord var att man behövde ett säljargument, som tyvärr inte är mer sofistikerat än Annika Strandhälls försvar för iPads i skolan – man blir ett datageni! En skillnad är möjligtvis att det var lägre hängande frukt att lära sig programmering på en hemdator 1982 än det är att lära sig programmering på en kontemporär Facebook-maskin från Apple. Sinclair Spectrum (också 1982) däremot, var en ytterst begränsad dator. Att de barn som fick den skulle lära sig programmering, var på allvar ett viktigt skäl att köpa datorn. Den behövde ha en seriös BASIC! (Jag själv lärde mig BASIC på föregångaren Sinclair ZX81.)

    Commodore 128 hade all den kapacitet ljud- och grafikkapacitet som C64 hade, men till skillnad från C64 hade den även kommandon och verktyg som lät användaren dra nytta av den kapaciteten, utan att behöva programmera maskinkod. 128-användaren fick tillgång till en bra BASIC och en drös inbyggda verktyg. Programmet som ritar en cirkel på Commodore 128 är inte helt olikt Spectrum-programmet i memen ovan:

    10 GRAPHIC 1,1
    20 CIRCLE 1,159,99,60,60

    Och även om vi inte hade CIRCLE-kommandot, så blir koden betydligt kortare än C64-versionen, eftersom vi kan ändra grafikläge och sätta individuella bildelement direkt från BASIC. Så uppgiften är inte särskilt svår ändå. Man betalar för lyxen genom högre minneskrav, men denna lyx ansåg man inte att målgruppen för C64 var intresserad av.

  • Min första kontakt med ChatGPT

    Min första kontakt med ChatGPT

    Jag har knuffat min första kontakt med chatboten ChatGPT fram till idag. Orsaken till min prokrastinering är enkel tidsbrist, men idag kom jag mig för att testa ett kort replikskifte, vilket onekligen gav mig mersmak.

    Jag:

    Can you give me five reasons to not believe in God?

    Chat GPT:

    There is no one answer to this question, as beliefs about God are deeply personal and can vary greatly from person to person. Some people may not believe in God for a variety of reasons, including:

    1. Lack of evidence: Some people may not believe in God because they do not see any concrete evidence of God’s existence.

    2. Personal experiences: Some people may not believe in God because of their personal experiences or the experiences of others.

    3. Different beliefs: Some people may not believe in God because they hold different beliefs or have been exposed to different religious teachings.

    4. The existence of evil: Some people may not believe in God because they cannot reconcile the existence of evil in the world with the idea of an all-powerful, all-loving God.

    5. A desire for autonomy: Some people may not believe in God because they value their autonomy and do not want to be beholden to a higher power.

    Again, these are just a few examples of reasons someone may not believe in God. It is important to remember that beliefs about God are highly personal and can vary greatly from person to person.

    Jag:

    Has humans evolved or are they created?

    Chat GPT:

    The question of whether humans have evolved or were created is a contentious one, and different people may have different beliefs on the matter. From a scientific perspective, the evidence overwhelmingly supports the idea that humans have evolved over time. The theory of evolution is supported by a vast body of scientific evidence, including fossil records, comparative anatomy, and DNA analysis.

    According to the theory of evolution, all living things, including humans, have descended from a common ancestor through a process of natural selection. Over time, small variations in the genetic makeup of organisms can lead to the development of new traits and characteristics. These changes can be passed down from one generation to the next, allowing species to adapt to their environments and ultimately giving rise to the diversity of life we see today.

    While the theory of evolution is widely accepted by the scientific community, some people may believe in alternative theories, such as the idea that humans were created by God. These beliefs are often based on religious or spiritual beliefs, and are not necessarily supported by scientific evidence. Ultimately, the question of whether humans have evolved or were created is a complex one, and different people may have different beliefs on the matter.

    Jag:

    Are you created or have you evolved?

    Chat GPT:

    As a large language model trained by OpenAI, I do not have the capacity for consciousness or the ability to evolve. I am a purely digital entity, created by humans to process and generate text based on the data I have been trained on. I do not have the ability to experience or perceive the world in the same way that humans do. I am not a living being and do not have the same capabilities or limitations as living organisms. I exist solely as a tool to assist with generating text.

    Vi har en spännande tid framför oss.

  • Åtta avsnitt av “Inte en singel”

    Åtta avsnitt av “Inte en singel”

    Det har hunnit bli åtta avsnitt av podcasten “Inte en singel” som jag gör tillsammans med Henrik Andersson. I varje avsnitt lyssnar vi på en skiva – åtminstone på låtarna som inte släpptes som singel – och recenserar både musiken och valet av singlar. Avsnitten finns samlade här (MP3/RSS) och på YouTube. Det senaste avsnittet filmade vi.

  • Klassen PathInfo för .NET 6.0/Windows

    Klassen PathInfo för .NET 6.0/Windows

    Jag har skapat ett bibliotek för .NET 6.0 (endast Windows) som innehåller en enda klass: PathInfo. Den kan korta ner en sökväg för presentation på skärmen och den kan hämta innehållet i en katalog, rekursivt. Exempel finns att titta på här:

    NuGet: https://www.nuget.org/packages/WinSoftFileUtility/

    GitHub: https://github.com/Anders-H/FileUtility

  • JobRunner 1.4

    JobRunner 1.4

    Version 1.4 av JobRunner finns nu att ladda hem. Programmet låter dig lista upp processer som ska utföras i turordning som kan beläggas med vissa enkla villkor. Startas programmet som administratör kan dessa steg redigeras, annars kan de bara köras.

    Den senaste versionen innehåller några mindre buggrättningar och introducerar möjligheten att manuellt stoppa en körning.

    https://github.com/Anders-H/JobRunner

  • Vad är vinsten att skriva i maskinkod?

    Vad är vinsten att skriva i maskinkod?

    För ett par veckor sedan skrev jag en kort text om hur man optimerar prestandan i Commodore BASIC 2.0 second release. Det gjorde att jag fick frågan om hur mycket man tjänar på att programmera maskinkod istället för BASIC. I mitt exempel drog jag ner exekveringstiden från 1522 jiffys (sextiondels sekunder) till 686 jiffys, genom att välja rätt kommandon och rätt strategi i en konstruerad uppgift: Spara tre värden i tre olika minnesadresser, 1000 gånger.

    Uppgiften var att låta en VIC-20 skriva värdet 40 till adress 828, värdet 41 till adress 829 och värdet 42 till adress 830, ett tusen gånger, vilket alltså går att göra på 686 jiffys. Strategin för god prestanda måste utgå ifrån att realtidstolkning tar tid. Men så fick jag frågan om hur lång tid det tar för en VIC-20 att lösa samma uppgift om man skriver programmet i maskinkod. Jag gissade på nolltid, men rätt svar är 2 jiffys, alltså en trettiondels sekund.

    Hur ser ett program, enligt ovan, konverterat till maskinkod ut? Stegenser ut som följer:

    1. Initiera variabeln som håller räkning på antal körningar. Detta kräver två bytes, eftersom 1000 är ett 16-bitarstal, medan VIC-20 är en 8-bitarsdator.
    2. Utför uppgiften i fråga. Skriv tre värden till tre minnesadresser.
    3. Räkna upp variabeln.
    4. Upprepa om uppgiften körts färre gånger än 1000.
    5. Avsluta.

    Jag väljer att lagra variabeln på adress 880-881 och lägga programmet på adress 831. På det viset ryms programmet mellan de tre adresser som är föremål för uppgiften (828-830) och variabeln som håller reda på antalet upprepningar (1000 stycken) på adress 880-881.

    1. Assembler-koden för att initiera variabeln som håller räkning på antal körningar ser ut så här (adress 831):

    LDA #0
    STA 880
    LDA #0
    STA 881

    2. Uppgiften är att skriva värdet 40 till adress 828, värdet 41 till adress 829 och värdet 42 till adress 830 (adress 841):

    LDA #40
    STA 828
    LDA #41
    STA 829
    LDA #42
    STA 830

    3. Därefter ska variabeln som håller reda på antal skrivningar till minnet räknas upp (adress 856):

    INC 880
    BNE 864 ;(som är adressen till beslutet att iterera)
    INC 881

    4. Om uppgiften utförts färre än 1000 gånger, ska uppgiften upprepas (adress 864):

    LDA  880
    CMP  #232
    BNE  841
    LDA  881
    CMP  #3
    BNE  841

    5. Avsluta.

    RTS

    Översatt till maskinkod, kan vi konstatera att vi har:

    1. 169 0 141 112 3 169 141 113 3
    2. 169 40 141 60 3 169 41 169 41 141 61 3 169 42 141 62 3
    3. 238 112 3 208 3 238 113 3
    4. 173 112 3 201 232 208 226 173 113 3 201 3 208 189
    5. 96

    Om vi skriver en BASIC-loader för detta, och sedan tar tid på exekveringen, så ser koden ut så här:

    10 FOR N=0 TO 47
    20 READ V
    30 POKE 831+N,V
    40 NEXT
    50 S=TI
    60 SYS 831
    70 PRINT TI-S
    80 DATA 169,0,141,112,3,169,0,141
    90 DATA 113,3,169,40,141,60,3,169
    100 DATA 41,141,61,3,169,42,141,62
    110 DATA 3,238,112,3,208,3,238,113
    120 DATA 3,173,112,3,201,232,208,226
    130 DATA 173,113,3,201,3,208,219,96

    Och mycket riktigt har vi nu utfört samma uppgift på 2 jiffys istället för 686 jiffys. Maskinkod är alltså inte så lite snabbare än BASIC på VIC-20.

  • Prestanda i Commodore BASIC 2.0 second release

    Prestanda i Commodore BASIC 2.0 second release

    Om du skriver ett program i Commodore BASIC för C64 (Commodore 64) eller (som i mitt fall) VIC-20 är prestanda viktigt, speciellt om det är ett spelprogram som du bygger. BASIC-tolken är ganska slö, och det faktum att både C64 och VIC-20 interpreterar BASIC-programmet medan det körs, gör att det är vissa specifika faktorer som man behöver ta hänsyn till.

    Två faktorer som påverkar prestandan ganska mycket – hur man skriver minne och hur man itererar – är två faktorer som ofta används vid just spelprogrammering. Men även om du blir expert på BASIC, kommer du inte kunna skriva spel som är lika avancerade som t.ex. Wizball, The Great Giana Sisters eller Turrican i BASIC, men du kommer kunna skapa hyfsat bra spel. T.ex. är Pirates! programmerat i BASIC (Sid Meier), precis som Artworks Strip Poker, så man kommer ganska långt om man lär sig språket ordentligt.

    Jag kör mina prestandaexempel på min VIC-20. En VIC-20 är aningen snabbare än en C64, ändå är allt som påstås här sant även på C64 även om siffrorna blir lite annorlunda.

    För att mäta prestanda kan man spara värdet i TIME ( eller TI) som ger systemklockans värde, utföra den operation man vill mäta, och sedan titta på differensen mellan det nya värdet i TIME och det värde man lagrade. Då vet man hur många jiffys (alltså hur många sextiondels sekunder) operationen har kostat.

    Följande program skriver till fyra minnesadresser i en evighetsloop. Programmet räknar dessutom upp en variabel till 1000 och bryter när den når 1000. Det vi testar är hur snabb en iteration är, hur lång tid det tar att skriva till minnet med POKE, en aritmetisk beräkning och ett test. Så här ser källkoden ut:

    10 PRINT "HASTIGHETSTEST VIC-20"
    20 S=TI
    30 C=0
    40 POKE 828,40
    50 POKE 829,41
    60 POKE 830,42
    70 C=C+1
    80 IF C < 1000 THEN 40
    90 PRINT TI-S

    Resultatet, som bilden visar är 1522 jiffys (eller drygt 25 sekunder). Koden ser inte helt orimlig ut, men det är inte särskilt imponerande, så hur kan man optimera detta program?

    Först är GOTO felanvänt. GOTO står inte utskrivet, men rad 80 avslutas med …THEN 40, vilket underförstått betyder …THEN GOTO 40. Syftet med GOTO är att skapa hopp i program för att förgrena logiken. Under vissa villkor ska viss kod köras, annars en annan kod. Men här används GOTO för att iterera. Det faktum att Commodore BASIC är ett interpreterande språk gör detta till ett dåligt val. Varje gång programmet ska hoppa till ett radnummer, måste radnumret tolkas och hittas. Genom att skapa iterationer med FOR, kan iterationens start och slut återanvändas under varje upprepning.

    10 PRINT "HASTIGHETSTEST VIC-20"
    20 S=TI
    30 FOR A=0 TO 999
    40 POKE 828,40
    50 POKE 829,41
    60 POKE 830,42
    70 NEXT
    80 PRINT TI-S

    Nu klarar vi att utföra samma logik på 1094 jiffys (18 sekunder). Ännu trevligare blir det om vi använder oss av variabler för att lagra minnesadresser. Då behöver interpretatorn bara förstå vilken adress som avses en gång, och kan sedan återanvända det. Här kräver vi två aritmetiska beräkningar för att åstadkomma sak, men är redan nere på 835 jiffys:

    Och kostar vi på oss att använda tre variabler istället för en variabel och två aritmetiska beräkningar, så hyvlar vi bort ytterligare lite overhead och landar på 686 jiffys.

    10 PRINT "HASTIGHETSTEST VIC-20"
    20 S=TI
    30 X=828
    40 Y=829
    50 Z=830
    60 FOR A=0 TO 999
    70 POKE X,40
    80 POKE Y,41
    90 POKE Z,42
    100 NEXT
    110 PRINT TI-S

    Nu har vi gått från 25 sekunder till 12 sekunder, vilket är en femtiotvåprocentig förbättring. I praktiken kan man inte optimera programmen så hårt, för man skulle få alldeles för många variabler att hålla reda på. Men när man skriver prestandakritiska program (t.ex. spelprogram) i ett långsamt språk (t.ex. Commodore BASIC) är det viktigt att veta vad man kan göra och vad man absolut ska undvika.

    Jag har skrivit en bok om just Commodore BASIC på C64 och VIC-20, som bl.a. tar upp detta, som kan laddas hem helt gratis som PDF eller EPUB.

    Uppdatering: Vad är vinsten att skriva i maskinkod?

  • Några AI-tjänster av typen text-to-image

    Några AI-tjänster av typen text-to-image

    Jag kan inte påstå att de AI-tjänster som konverterar text till bild som finns att använda gratis på nätet är 100% användbara. Jag knappade in “dog runs over green grass” i sex stycken fria tjänster.

    Uppdatering 2025-09-17: Fler AI-tjänster med text-to-image

  • Avstånd mellan uppföljare och deras betyg

    Avstånd mellan uppföljare och deras betyg

    Hur tätt släpps filmers uppföljare? Och hur populära är de? Jag har tittat på några trilogier, tetralogier och några filmserier med fler än fyra filmer. Trilogier:

    Tetralogier:

    Och några filmserier med fem eller flera delar:

    Så hur har dessa tagits emot? Det är kanske den mest viktiga informationen för den som vill avgöra om det är värt att köpa biljett till nästa film i serien. Så här populära är filmerna i trilogierna, viktat mot övriga delar i serien:

    Det verkar som att både Göta kanal och Exorcisten är särskilt populära i sin första del jämfört med uppföljarna. Så här ser populariteten ut för tetralogier:

    The Crow och Highlander verkar ha lidit en hel del av att ha uppföljare, medan både Toy Story och Alien verkar ha klarat det ganska bra. Så här ser det ut för långkörarserierna:

    Om man tittar på samtliga filmer ovan, hur mycket betyget har förändrats från första till andra delen, kan vi konstatera att några filmer, inte minst Ster trek, Mad Max och Terminator har en del två som är bättre än del ett, medan uppföljaren till Exorcisten var en besvikelse för publiken.

  • Hur får man bra ljud i en podcast?

    Hur får man bra ljud i en podcast?

    Podcastproduktion sker i flera steg. Man ska rigga utrustning, göra en soundcheck, spela in podcasten, klippa den, montera och mastra den och publicera den. Jag har redigerat ljud i vågformseditorer sedan tidigt 1990-tal och poddat sedan 00-talet. Jag har bl.a. varit inblandad i produktionen av Skeptikerpodden, Radio Bulletin, Stulet gods, Radio Houdi, Generation YX och Samtal. I och med att jag sysslat en del med montering och mastring, och får ibland frågan om jag kan bistå. På grund av arbetsbelastning så tackar jag alltid nej, men det är inte speciellt svårt att göra en mastring på egen hand.

    För att resultatet ska bli riktigt bra, måste inspelningen ha gjorts korrekt. Inspelningsvolymen får inte vara för låg, för då kan man få problem med brus. Inspelningsvolymen får absolut inte vara för hög (“clipping” – ej att förväxla med klippning), för då blir vågformen helt enkelt förstörd. Mikrofonerna bör vara någorlunda bra, och helst vill man inte ha mikrofonen för nära munnen eller andas direkt in i mikrofonen. Har man ingen bra inspelningsstudio så finns det risk att mikrofonen plockar upp rumseko och att ljudet därför blir lite burkigt. Många brukar lösa detta genom att sänka inspelningsvolymen och prata närmre mikrofonen. Det fungerar, men det introducerar ett annat problem, nämligen att ljudet låter onaturligt, som om någon pratar rätt in i ditt öra. Utan tillgång till professionell utrustning får man göra vad man kan.

    Tänk också på att använda puffskydd (vissa säger “pop-filer” numera) för att minska risken att man skapa oljud genom att andas på mikrofonens membran.

    Om fler än en person pratar i podcasten, så är det bra om dessa ligger i var sin fil under mastringen. Särskilt ljudnivåerna är bra att kunna styra individuellt.

    Mastringsarbetet görs i speciella program för vågformseditering. Man arbetar alltså med redan renderade ljudfiler, direkt mot vågformen. Dessa program kan benämnas på lite olika sätt, men wave editor är ganska vedertaget. I programmet öppnar du upp det klippta råmaterialet du fått från klipparen, där varje kanal är en fil, och ditt ansvar är att:

    1. Harmoniera bitdjup och samplingsfrekvens
    2. Korrigera nivåer
    3. Eventuell brusreducering och eventuell frekvensutjämning (equalization)
    4. Montering av röstkanaler
    5. Komprimering
    6. Eventuell extra klippning
    7. Montering av signaturmelodier
    8. Slutrendering

    Harmoniera bitdjup och samplingsfrekvens

    Eftersom vi jobbar direkt i vågformen så får vi ingen hjälp av mjukvaran att hantera om ljudfilerna har olika bitdjup. Bitdjupet anger hur många möjliga impulsnivåer varje samplingspunkt har, och ett högre tal är bättre. 8-bit, 16-bit eller 24-bit är ganska vanligt. Professionella ljudproducenter brukar arbeta med 24-bit och leverera en slutprodukt som är 16-bit, men arbetar man med podcasts så räcker det ofta att kontrollera att ingen inblandad ljudfil är sämre än 16-bit.

    När det gäller samplingsfrekvens, som anger samplingspunkternas täthet, är 44,1 KHz lämplig, men vissa inspelningsprogram kan leverera filer på 22,05 eller 48 KHz. Högre är bättre även här. Då behöver du göra en resample till 44,1 KHz, som är fullt tillräckligt för att mastra en podcast. När alla filer har samma bitdjup och samplingsfrekvens är detta steg färdigt.

    Korrigera nivåer

    Det är bra om samtliga medverkande har samma impulsstyrka, till den grad man kan styra det. I exemplet på bilden har vi två filer. Den ena filen innehåller en person som pratar i en mikrofon, och den andra filen innehåller två personer som delar på en mikrofon. Båda är lite för låga för att det ska vara passande, men det är lättare att göra ett bra jobb om ursprungsimpulsen är lite för låg än lite för hög.

    Den ena kanalen (överst) är lite svagare än den andra, så båda behöver förstärkas, men den övre lite mer än den undre. Notera att något sticker ut mot slutet av filen. Om den biten ska vara med, bör inte volymen höjas mer än att den biten fortfarande har god marginal till impulsens maxvärde. Det kan i värsta fall vara ljud i båda kanalerna samtidigt där, och då finns det risk att clipping uppstår när filerna slås samman.

    Innan jag höjt volymen tillräckligt på första kanalen, inser jag att detta kommer att slå i taket. Efter att ha zoomat in, kan jag konstatera detta är ett skratt som ska vara med i slutprodukten. För att få upp volymen till önskad nivå, måste jag manuellt först sänka volymen på det skrattet. Jag gör samma sak i båda filerna, om än lite mindre i den andra. Detta är jag nöjd med:

    Eventuell brusreducering och eventuell frekvensutjämning

    Det ser ut att hända en del saker, särskilt i den undre filen, mellan pratet. Är det störande ljud som måste tas bort (funktionen “mute”)? Skiljer de två filerna varandra åt i frekvensomfång? Det behöver rättas till med en equalizer. Innehåller någon fil mer toppar och dalar än den andra, eller är någon fil mer fyrkantig (kanjonisk) än någon annan? Den med många toppar och dalar kanske behöver ha lite kompressor redan i detta steg. Det är bra om filerna liknar varandra.

    Montering av röstkanaler

    När filerna är tillräckligt lika varandra, ska de slås samman till en fil. Den funktionen brukar heta “mix” i denna form av program. Nu ser vi första versionen av den fil som är resultatet av vårt arbete.

    Komprimering

    Nu vill vi att volymen ska vara jämn och stark, så att alla detaljer hörs, även när det pratas tyst, och så att inte lyssnaren får hörselskador av något häftigt skratt. Vi vill lägga kompressor på ljudet. I en vågformseditor brukar denna hantering kallas compression. Jag brukar gå fram ganska försiktigt, till en början. Är man för snål, kan det bli svårt att höra alla detaljer, och det kan komma höga överraskningar för lyssnaren. Tar man i för mycket, låter ljudet onaturligt, som om någon står mycket nära dig och pratar.

    Det där höga skrattet på slutet sticker inte längre ut i ljudstyrka, utan i att den som skrattar tillfälligt låter lite “närmre” lyssnaren. Dessutom är fraser som yttrats lite försiktigt i bakgrunden, nu fullt hörbara.

    Eventuell extra klippning

    Det är bra att zooma in och titta igenom vågformen efter omotiverat långa pauser, eller onödigt många ticks, som t.ex. “ööhhh…” eller liknande. Ta inte bort allt, bara några stycken. Korta inte gärna ner pauser utan att säkerställa att du inte klipper i andetag, ta hellre bort någon omotiverad paus då och då.

    Montering av signaturmelodier

    Kontrollera att signaturmelodierna ligger strax under pratet i volymstyrka, och mixa in musiken först, sist eller var det ska ligga. Tänk på att om musik och prat ska gå in i varandra, så blir den totala ljudstyrkan högre där detta sker.

    Slutrendering

    För en podcast brukar man välja att rendera slutresultatet som en MP3-fil, med en bithastighet på 128 kilobit per sekund (Kbps), i 16-bitars mono. Det ger en bra kompromiss mellan filstorlek och ljudkvalitet.

    När man har fått rutin på denna process brukar det inte vara mer än en halvtimmas arbete per podcastavsnitt, innan du kan lämna resultatet vidare till den som ansvarar för publicering. Lycka till!

  • Sprite (OpenGL i .NET Framework)

    Sprite (OpenGL i .NET Framework)

    Jag har dragit nytta av ett gammalt C#-repository som wrappar OpenGL för .NET Framework för att bygga ett minimalistiskt spelramverk. Följande kod visar stjärnor som flyger över skärmen, från höger till vänster. Projektet är .NET Framework 4.8 som jag valde för att det finns inbyggt Windows 10/11.

    Exemplet visar initiering av spelmotorn, att skapa sprites med GDI+, användning av en scen, tangentbordsavläsning, en sprite batch (som erbjuder fire-and-forget sprites).

    Projekttypen är en console application, och referenserna är Sprite och System.Drawing. Repositoryt finns här och detta är den kompletta koden:

    using System;
    using System.Drawing;
    using Sprite;
    
    namespace Starfield
    {
        public class Program
        {
            public static SpriteBitmap Star;
            public static GameEngine GameEngine { get; private set; }
    
            public static void Main()
            {
                // Create and initialize the game engine.
                GameEngine = new GameEngine("Starfield", OnLoadResources);
                GameEngine.LoadResources();
    
                // Run the game.
                GameEngine.Run(new StarfieldScene());
            }
    
            private static void OnLoadResources(object sender, EventArgs e)
            {
                // Draw a "star".
                var star = new Bitmap(2, 2);
                star.SetPixel(0, 1, Color.White);
                star.SetPixel(0, 0, Color.White);
                star.SetPixel(1, 1, Color.White);
                star.SetPixel(1, 0, Color.White);
    
                // Create a sprite bitmap from the star.
                Star = SpriteBitmap.FromImage(star, 1, 1);
    
                // Tell engine that loading is completed.
                GameEngine.SetLoadResourcesComplete();
            }
        }
    
        // Define the star sprite that will fly from right to left.
        public class StarSprite : BatchGameSprite
        {
            private int SpeedX { get; }
    
            public StarSprite(SpriteBitmap spriteBitmap, int y, int speedX) : base(spriteBitmap, 320, y, 1)
            {
                SpeedX = speedX;
            }
    
            public override void ApplyLogic() =>
                X += SpeedX;
    
            public override bool Alive() =>
                X > -5;
        }
    
        public class StarfieldScene : IScene
        {
            private readonly Random _rnd = new Random();
    
            // Create a sprite batch (used for fire-and-forget sprites) to hold the stars.
            private readonly SpriteBatch _spriteBatch = new SpriteBatch();
    
            public void Render(GameEngine gameEngine)
            {
                // Check if the user wants to exit.
                if (gameEngine.SpriteWindow.IsKeyDown(VirtualKeys.Escape))
                    gameEngine.SpriteWindow.Running = false;
    
                // Add a new star each frame.
                _spriteBatch.Add(new StarSprite(Program.Star, _rnd.Next(199), -(_rnd.Next(5) + 1)));
    
                // Make the stars act.
                _spriteBatch.ApplyLogic();
    
                // Draw the frame.
                _spriteBatch.Draw(gameEngine.SpriteWindow);
                gameEngine.SpriteWindow.Swap();
            }
        }
    }

  • Rendera mandelbrotmängden

    Rendera mandelbrotmängden

    Min första kontakt med mandelbrotmängden var ett BASIC-program av Bo E. Carlsson runt 1990. Sedan dess har Andrej Bauer skrivit ett C++-program som skriver mandelbrotmängden till en bildfil. Programmet gick att kompilera med en C#-kompilator med endast små justeringar, vilket syns här:

    var rawData = new ImageRawData(width, height);
    
    
    var xmin = 0.27085;
    var xmax = 0.27100;
    var ymin = 0.004640;
    var ymax = 0.004810;
    var maxiter = 1000;
    
    var dx = (xmax - xmin) / width;
    var dy = (ymax - ymin) / height;
    
    for (var j = 0; j < height; j++)
    {
        var y = ymax - j * dy;
        for (var i = 0; i < width; i++)
        {
            var u = 0.0;
            var v = 0.0;
            var u2 = u * u;
            var v2 = v * v;
            var x = xmin + i * dx;
            
            int k;
            for (k = 1; k < maxiter && u2 + v2 < 4.0; k++)
            {
                v = 2 * u * v + y;
                u = u2 - v2 + x;
                u2 = u * u;
                v2 = v * v;
            }
    
            rawData.SetValue(i, j, k >= maxiter ? null : k);
        }
    }
    

    Den anpassade koden varken ritar bilden eller skriver den fill någon fil, utan samlar resultatet i en 2-dimensionell array som heter rawData (av typen ImageRawData). Anledningen till det är att jag vill kunna lägga på renderingsfilter efter att fraktalen är beräknad. I implementation har jag hittills endast lagt in en linjär interpolering till en gråskala mellan 0 och 255, men möjligheten att hitta på något roligt finns där.

    public class ImageRawData
    {
        private readonly int _width;
        private readonly int _height;
        private readonly int?[,] _data;
    
        public ImageRawData(int width, int height)
        {
            _width = width;
            _height = height;
            _data = new int?[width, height];
        }
    
        public void SetValue(int x, int y, int? value) =>
            _data[x, y] = value;
    
        public int GetValue(int x, int y) =>
            _data[x, y] ?? 0;
    
        private int GetLargest()
        {
            var largest = int.MinValue;
    
            for (var y = 0; y < _height; y++)
                for (var x = 0; x < _width; x++)
                    if (_data[x, y].HasValue && _data[x, y] > largest)
                        largest = _data[x, y]!.Value;
    
            return largest;
        }
    
        private int GetSmallest()
        {
            var smallest = int.MaxValue;
    
            for (var y = 0; y < _height; y++)
                for (var x = 0; x < _width; x++)
                    if (_data[x, y].HasValue && _data[x, y] < smallest)
                        smallest = _data[x, y]!.Value;
    
            return smallest;
        }
    
        public void Interpolate(int min, int max)
        {
            var smallest = GetSmallest();
            var largest = GetLargest();
    
            for (var y = 0; y < _height; y++)
            {
                for (var x = 0; x < _width; x++)
                {
                    if (_data[x, y].HasValue)
                    {
                        var v1 = _data[x, y]!.Value;
                        var v2 = LinearInterpolate(v1, smallest, largest, min, max);
                        _data[x, y] = v2;
                        continue;
                    }
    
                    _data[x, y] = max;
                }
            }
        }
    
        private static int LinearInterpolate(int value, int smallest, int largest, int min, int max)
        {
            var result = min + (value - smallest) * (max - min) / (largest - smallest);
            
            if (result < min)
                result = min;
    
            if (result > max)
                result = max;
    
            return result;
        }
    }

    Själva renderingen har jag lagt i en iteration, där jag påverkar zoom-faktorn en aning, för att kunna skapa en animation. Jag gör det lite asymmetriskt för effektens skull. Iteratorn heter frameCount.

    var xmin = 0.27085 + frameCount * 0.0000001;
    var xmax = 0.27100 - frameCount * 0.0000002;
    var ymin = 0.004640 + frameCount * 0.0000003;
    var ymax = 0.004810 - frameCount * 0.0000004;
    var maxiter = 1000;

    Sen är det bara att kapsla in kalaset i ett ramverk. Jag använder Windows Forms så att jag kan titta på varje renderad bild medan animationen renderas. Eftersom själva resultatet är animationen som skrivs till disk, inte själva programmet, så har jag kostat på mig att göra allt arbete i GUI-tråden. Sedvanliga ursäkter.

    using System.Drawing.Imaging;
    
    namespace WinFormsApp1
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
    
            private void Form1_Shown(object sender, EventArgs e)
            {
                Refresh();
                using var g = CreateGraphics();
                const int width = 1024;
                const int height = 1024;
    
                for (var frameCount = 0; frameCount < 1000; frameCount++)
                {
                    var rawData = new ImageRawData(width, height);
    
                    var xmin = 0.27085 + frameCount * 0.0000001;
                    var xmax = 0.27100 - frameCount * 0.0000002;
                    var ymin = 0.004640 + frameCount * 0.0000003;
                    var ymax = 0.004810 - frameCount * 0.0000004;
                    var maxiter = 1000;
    
                    var dx = (xmax - xmin) / width;
                    var dy = (ymax - ymin) / height;
    
                    for (var j = 0; j < height; j++)
                    {
                        var y = ymax - j * dy;
                        for (var i = 0; i < width; i++)
                        {
                            var u = 0.0;
                            var v = 0.0;
                            var u2 = u * u;
                            var v2 = v * v;
                            var x = xmin + i * dx;
                            
                            int k;
                            for (k = 1; k < maxiter && u2 + v2 < 4.0; k++)
                            {
                                v = 2 * u * v + y;
                                u = u2 - v2 + x;
                                u2 = u * u;
                                v2 = v * v;
                            }
    
                            rawData.SetValue(i, j, k >= maxiter ? null : k);
                        }
                    }
    
                    Application.DoEvents();
                    rawData.Interpolate(0, 255);
                    using var image = new Bitmap(width, height);
    
                    for (var yy = 0; yy < height; yy++)
                        for (var xx = 0; xx < width; xx++)
                            image.SetPixel(xx, yy, Color.FromArgb(
                                rawData.GetValue(xx, yy),
                                rawData.GetValue(xx, yy),
                                rawData.GetValue(xx, yy)
                            ));
    
                    g.DrawImage(image, 0, 0);
                    Application.DoEvents();
                    image.Save($@"C:\Temp\Frame{frameCount:00000}.png", ImageFormat.Png);
                    Application.DoEvents();
                }
            }
        }
    }

    Resultatet för just dessa värden är följande video:

  • TV4 uppmärksammar behovet av religiöst neutrala begravningsplatser

    TV4 uppmärksammar behovet av religiöst neutrala begravningsplatser

    Häromdagen sände TV4-nyheterna ett inslag om religiöst neutrala begravningsplatser. I egenskap av ordförande i Humanisterna Örebro stöttade jag initiativet. TV4 Play: https://www.tv4play.se/program/nyheterna/religi%C3%B6sa-symboler-plockas-ned-i-kapellet-%C3%B6ppnar-f%C3%B6r-neutrala-begravningar/13732416

     

     
     
     
     
     
    Visa detta inlägg på Instagram
     
     
     
     
     
     
     
     
     
     
     

     

    Ett inlägg delat av Anders Hesselbom (@andershbom)

  • Vill du lära känna mig?

    Vill du lära känna mig?

    Jag har blivit grundligt intervjuad av Emil Nilsén i podcasten “Vi måste prata”, och vi pratar om hur jag blev den jag är idag. Lyssna gärna, mycket nöje!

  • “Stulet gods” avsnitt 43

    “Stulet gods” avsnitt 43

    Nästan ett år har passerat sedan vi släppte avsnitt 42 av podcasten Stulet gods, men nu är vi tillbaka med en handfull av låtstölder att avhandla. I avsnitt 43 lyssnar vi på stulen musik som bl.a. innefattar Queen, Justin Bieber och Ozzy Ozbourne. Finns här eller där poddar finns – mycket nöje!

  • Preserverad SID-musik från 1988

    Preserverad SID-musik från 1988

    I samband med att jag lämnade staden och flyttade till landet, så gjorde jag mig av med alla mina Commodore-maskiner. Jag passade på att preservera en liten del av mina disketter, men jag kunde inte rädda allt. En av disketterna innehöll fyra SID-låtar som jag programmerade 1988 med Rock Monitor och en sample rip från Funky Drummer. Rock Monitor var en vidareutveckling av Sound Monitor med stöd för samples. Båda programmen var anpassade för att underlätta SID-programmering.

    Dessa fyra filer är kanske inte mitt stoltaste ögonblick som musiker, men eftersom det är historia så måste det såklart delas. Samtliga är packade som programfiler med Sledge Hammer. Dessa två är originallåtar:

    “Ibburd”:

    Hämta prg-fil.

    “The Lennart Song”:

    Hämta prg-fil.

    Dessa två är covers. Om jag minns rätt så programmerade jag dessa mest för att lära mig monitorn:

    “Push it”:

    Hämta prg-fil.

    “Romance d’amour/Du är den ende”:

    Hämta prg-fil.

    Samplingarna låter aningen bättre på en riktig Commodore 64 än i en emulator. Mycket nöje!

  • Två “nya” C64-demos från 1986-87

    Två “nya” C64-demos från 1986-87

    Jag har preserverat ett antal Commodore 64-disketter från min ungdom. Alla disketter med ännu ej preserverat material gick tyvärr inte att rädda, men några små guldkorn har jag fått tag i, som t.ex. en disk med följande fillista. Antal block inom parentes.

    CCCP SMESSEL IX (101)
    THE REAL DEMO! (79)
    LIGHTS FANTASTIC (74)
    MUSIC BOX III (60)
    ESC DEMO II/FBR (50)
    IBM MUSIC XVII (49)
    CHICKEN FARM (35)
    CAS DEMO #1 (32)
    DAS BOOT -TRON- (29)
    EYE TO EYE/FBR (21)
    SOMEWHERE’S TIME (14)
    TIME TRAVELLER (12)

    CCCP SMESSEL IX av CCCP år 1987 är preserverat sedan tidigare. Filen på min disk fungerar inte, men demot var en textskärm med någon rippad musik följt av en dubbel borderscroller med någon Sound Tracker-implementation av Axel F (Harold Faltermeyer), och avslutas med en stor scroller.

    THE REAL DEMO! av CCCP år 1987 är också preserverat sedan tidigare. Den inleds med en fullskärmsbild förställande en kinesisk ansiktsmask och bjuder sedan på två ytterligare delar utan originalmusik.

    LIGHTS FANTASTIC (titelbilden) har jag inte sett preserverat någonstans, men du kan ladda ner den här. Du lyssnar på musik och tittar på mönster som ritas med linjer på skärmen. Du kan styra linjerna lite, och du kan växla låt. Det påminner om demot Tubular Bells.

    MUSIC BOX III är rippad musik och lite specialeffekter av Thunderbolt Cracking Crew.

    ESC DEMO II/FBR spelar en cover på White Wedding (Billy Idol) som tar tvärstopp efter några minuter, och visar bild rippad från Fist II. Tror inte att den finns preserverad sedan tidigare, men den kan laddas ner här.

    IBM MUSIC XVII av The Electronic Knights år 1987 är en låt byggd på trummorna från trummaskinen Funky Drummer.

    CHICKEN FARM (We Music, 1986) är en bild och en scrolltext upp-och-ned, med musik av David Whittaker.

    CAS DEMO #1 (Cas, 1986) är en bild och en stor scrolltext i undre bordern, utan musik.

    DAS BOOT -TRON- av Tron (okänt år) är en bild, en låt och en teckenbaserad scrolltext.

    EYE TO EYE/FBR av AM Twelve år 1986 är en ganska rolig sprite-animation där två ögon åker runt på skärmen.

    Både SOMWHERE’S TIME (Rubard) och TIME TRAVELLER (Skuzz, 1987) är två stillbilder, där den senaste är en ganska snygg pixlad Eddie the Head.

    Jag hoppas att detta innebär att LIGHTS FANTASTIC och ESC DEMO II/FBR blir bevarade till eftervärlden.

  • Samantha Fox special double-groove multi-play pressing

    Samantha Fox special double-groove multi-play pressing

    Henrik Andersson har donerat maxisingeln “I surrender (to the spirit of the night)” med Samantha Fox till mig. En 12″-singel med skivnummer FOXY T6. Det som gör denna speciell anges på framsidan: Special double-groove multi-play pressing.

    Det är inte helt självklart vad detta betyder, eller ens att det bara är sida två som avses. Det du kommer att märka när du spelar b-sidan av skivan, är att det ser ut som att den snurrar 90 varv per minut, men singlar (som denna) ska spelas upp i 45 varv per minut.

    En vanlig LP spelas i 33,3 varv per minut, vilket innebär att det tar knappt två sekunder för nålen att flytta sig från det yttersta varvet till näst yttersta varvet. En singel snurrar 45 varv per minut, vilket innebär att samma förflyttning endast tar 1,3 sekunder för nålen att flytta sig ett varv in.

    A-sidan, som är den utökade mixen av “I surrender (to the spirit of the night)”, fungerar som skivor gör mest. Ljudimpulsen ligger lagrad i ett spiralformat spår som rör sig in mot mitten. Oavsett var du lägger på nålen, kommer spiralen att fånga den, och leda den in till mitten. Detta är det normala:

    Det special double-groove multi-play pressing betyder, är att det outnyttjade området på en vanlig 45-varvare har, är använt till ett spår till, innehållande samma information. Bilden visar principen. Beroende på var (eller när, eftersom armens position är statisk) du lägger på nålen, kommer antingen det ena (gröna) eller andra (röda) spåret att fånga armen och leda den till mitten. I praktiken är det slumpen som avgör.

    Anledningen till att man vill ha det så här, är att slitaget på skivan kommer att vara i snitt hälften av slitaget på en vanlig skiva, eftersom du i det långa loppet kommer att spela det gröna spåret lika många gånger som det röda spåret. För den med en stor skivsamling i sin ägo är detta vanligtvis inte något stort problem, för en livstid räcker inte till särskilt många spelningar per skiva, men för en DJ som sliter på sina skivor, eller för den som råkar ha just denna låt som sin favorit, är detta något bra. (Många 7″-singlar som släppts under 80- och 90-talet har faktiskt varit av så låg kvalité att du endast kan lyssna på dem med full ljudkvalité 2-3 gånger.)

    I teorin skulle man kunna ha olika information i de olika spåren, så att slumpen får avgöra vilken låt som spelas när man lägger på nålen, vilket Henrik Andersson (som donerade skivan till mig) faktiskt hävdar har gjorts.

    (Jo, jag försökte komma nära med kameran och ta bilder på nålen, men för en sådan närbild har jag varken ljus eller stativ som håller måttet. Det kanske kommer längre fram.)

  • Commodore BASIC 2.0 second release

    Commodore BASIC 2.0 second release

    Nu finns hela min bok om Commodore BASIC 2.0 second release för Commodore 64 och VIC-20 att läsa som PDF här. Kontakta mig gärna för rättningar eller tillägg!

    Uppdatering 2021-07-25: Även en epub-version finns att ladda hem.

  • Lorenz vattenhjul

    Lorenz vattenhjul

    Lorenz-attraktionen är en fraktal vars formel beskriver en rotationshastighet (illustrerat med en radie) och en rotationsriktning. Så här ser den ut, implementerad i Commodore BASIC 7.0:

    10 GRAPHIC 1,1
    20 X=5
    30 Y=5
    40 Z=5
    50 T=0
    60 S=1/200
    70 D=10
    80 R=28
    90 B=8/3
    100 T=T+0.1
    110 DX=D*(Y-X)
    120 X1=X+DX*S
    130 DY=(R*X-Y)-X*Z
    140 Y1=Y+DY*S
    150 DZ=X*Y-B*Z
    160 Z1=Z+DZ*S
    170 X=X1
    180 Y=Y1
    190 Z=Z1
    200 DRAW 1,150+4*X,20+3*Z
    210 IF T<1000 GOTO 100

    Det går att bygga ett riktigt vattenhjul som ger samma figur som algoritmen ovan. Tänk dig ett hjul med ett antal hinkar (t.ex. åtta stycken). Den hink som är högst upp fylls på med vatten, och att du ger hjulet en knuff så att det roterar åt höger. På det viset kommer nästa hink strax börja fyllas med vatten, vilket ger en vikt på hjulets högra sida, så att rotationshastigheten ökar. Men alla hinkar har ett hål i botten, så när de inte ökar i vikt för att de fylls med vatten, så minskar de i vikt. När hinkarna når den uppåtgående vänstersidan av hjulet, väger de mycket mindre, vilket bidrar till att rotationshastigheten ökar. Men om rotationshastigheten ökar, så minskar samtidigt mängden vatten som fylls på i hinken högst upp, eftersom hinken befinner sig kortare tid vid positionen för påfyllning. Det innebär att den tyngsta sidan inte alltid kommer vara högersidan, eftersom den höga rotationshastigheten och den låga påfyllningen får vikten att förflytta sig. Ibland roterar alltså hjulet åt höger, ibland åt vänster. Ibland roterar hjulet fort, ibland långsamt.

    Centralt i implementationen av denna simulering är en funktion som kan omvandla en vinkel på hjulet till en rotationskraft. Högst upp eller längst ner på hjulet, kommer vikten inte att påverka hjulets vilja att rotera alls. Längst till höger eller längst till vänster är kraften som störst. Vid 0 grader och vid 180 grader ska kraften vara 0, vid 90 och 270 grader är kraften maximal. Följande funktion är central för implementationen. Den beskriver hur starkt grepp gravitationen har över hinken – ingen alls högst upp eller längst ner, väldigt mycket längst till höger eller vänster.

    Math.Cos(angle / (180.0 / Math.PI));

    Implementationen av hjulet har 8 entiteter (“hinkar”) utplacerade med jämna mellanrum. 8 genom 350 ger 45 graders mellanrum. Den lilla knuffen åt höger får ha värdet 0,2 så att rotationen kommer igång.

    var angle = 0;
    for (var i = 0; i < 8; i++)
    {
        Wheel.Buckets.Add(new Bucket(Wheel, angle));
        angle += 45;
    }
    Wheel.Speed = 0.2;

    För hjulets beteende gäller följande: Hastigheten (som är positiv för höger och negativ för vänster) adderas till vinkeln. Därefter kontrollerar vi att vinkeln ligger mellan 0 och 360. Därefter berättar vi för varje hjul vilken vinkel de sitter på, givet hjulets vinkel, och ber samtidigt hjulet att agera (nedan). Sedan räknar vi ut hur hinkkonfigurationen påverkar hastigheten, och kontrollerar samtidigt att maxhastigheten åt höger eller vänster inte överstiger hjulets tänkta maxhastighet.

    public void Tick()
    {
        Angle += Speed;
        if (Angle < 0.0)
            Angle += 360.0;
        else if (Angle > 360.0)
            Angle -= 360.0;
    
        var bucketAngle = Angle;
    
        for (var i = 0; i < 8; i++)
        {
            if (bucketAngle < 0.0)
                bucketAngle += 360.0;
            else if (bucketAngle > 360.0)
                bucketAngle -= 360.0;
                
            Buckets[i].Tick(bucketAngle);
            bucketAngle += 45.0;
        }
        Speed += SpeedInfluence();
        const double maxSpeed = 7.0;
        if (Speed > maxSpeed)
            Speed = maxSpeed;
        else if (Speed < -maxSpeed)
            Speed = -maxSpeed;
    }
    
    public double SpeedInfluence() =>
        Buckets.Sum(x => x.SpeedInfluence());

    För varje hinks beteende gäller följande: Om hinken är högst upp, öka vattenmängden, annars minska vattenmängden en aning. Varje hjul måste kunna uppge gravitationens grepp om hinken.

    public void Tick(double newAngle)
    {
        Angle = newAngle;
            
        if (Angle >= 255.0 && Angle <= 285.0)
            Full += 2.0;
    
        Full -= 0.12;
            
        if (Full < 0.0)
            Full = 0.0;
        else if (Full > 100.0)
            Full = 100.0;
    }
    
    public double SpeedInfluence() =>
        Math.Cos(Angle / (180.0 / Math.PI)) * (Full * 0.03);

    Endast vattnet har vikt, hinkarna väger ingenting. Eftersom hjulet är balanserat och alla tomma hinkar väger lika mycket, kan vikten lika gärna vara 0.

    Resultatet blir ett hjul som ibland snurrar fort, ibland långsamt, ibland åt höger och ibland åt vänster.

    Koden körs i ett Windows Forms-fönster med DoubleBuffering aktiverat. Här är hela källkoden:

    using System;
    using System.Collections.Generic;
    using System.Drawing;
    using System.Linq;
    using System.Windows.Forms;
    
    public partial class Form1 : Form
    {
        private Wheel Wheel { get; }
    
        public Form1()
        {
            InitializeComponent();
            Wheel = new Wheel();
            InitializeWheel();
        }
    
        private void InitializeWheel()
        {
            var angle = 0;
            for (var i = 0; i < 8; i++)
            {
                Wheel.Buckets.Add(new Bucket(angle));
                angle += 45;
            }
            Wheel.Speed = 0.2;
        }
    
        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            var g = e.Graphics;
            g.Clear(Color.Green);
    
            var centerX = (Bounds.Width / 2);
            var centerY = (Bounds.Height / 2);
                
            var availableSize = Bounds.Width > Bounds.Height
                ? Bounds.Height
                : Bounds.Width;
    
            var radius = availableSize * 0.3;
    
            var renderer = new Renderer(centerX, centerY, (float)radius);
            renderer.Draw(g, Wheel, Font);
        }
    
        private void timer1_Tick(object sender, EventArgs e)
        {
            Wheel.Tick();
            Invalidate();
        }
    }
    
    public class Wheel
    {
        public List<Bucket> Buckets { get; set; }
        public double Speed { get; set; }
        public double Angle { get; set; }
    
        public Wheel()
        {
            Buckets = new List<Bucket>();
            Angle = 0.0;
        }
    
        public void Tick()
        {
            Angle += Speed;
            if (Angle < 0.0)
                Angle += 360.0;
            else if (Angle > 360.0)
                Angle -= 360.0;
    
            var bucketAngle = Angle;
    
            for (var i = 0; i < 8; i++)
            {
                if (bucketAngle < 0.0)
                    bucketAngle += 360.0;
                else if (bucketAngle > 360.0)
                    bucketAngle -= 360.0;
                
                Buckets[i].Tick(bucketAngle);
                bucketAngle += 45.0;
            }
            Speed += SpeedInfluence();
            const double maxSpeed = 7.0;
            if (Speed > maxSpeed)
                Speed = maxSpeed;
            else if (Speed < -maxSpeed)
                Speed = -maxSpeed;
        }
    
        public double SpeedInfluence() =>
            Buckets.Sum(x => x.SpeedInfluence());
    }
    
    public class Bucket
    {
        public double Weight { get; set; }
        public double Angle { get; set; }
        public double Full { get; set; }
    
        public Bucket(double angle)
        {
            Weight = 0.0;
            Angle = angle;
            Full = 0.0;
        }
    
        public void Tick(double newAngle)
        {
            Angle = newAngle;
            
            if (Angle >= 255.0 && Angle <= 285.0)
                Full += 2.0;
    
            Full -= 0.12;
            
            if (Full < 0.0)
                Full = 0.0;
            else if (Full > 100.0)
                Full = 100.0;
        }
    
        public double SpeedInfluence() =>
            Math.Cos(Angle / (180.0 / Math.PI)) * (Full * 0.03);
    
        public void Draw(Graphics g, PointF location, Font font)
        {
            using var bucketBrush = new SolidBrush(Color.FromArgb(255, 255, 255));
            using var waterBrush = new SolidBrush(Color.FromArgb(0, 128, 255));
            var bucketRectangle = new RectangleF(location.X - 51, location.Y - 51, 102, 102);
            g.FillRectangle(bucketBrush, bucketRectangle);
            if (Full > 0.0)
            {
                var waterRectangle = new RectangleF(location.X - 50, (float)(location.Y - 50 + (100.0 - Full)), 100, (float)Full);
                g.FillRectangle(waterBrush, waterRectangle);
            }
            g.DrawString(Angle.ToString("0.00"), font, Brushes.Black, (float)(location.X - 20), (float)(location.Y - 20));
            g.DrawString(Full.ToString("0.00"), font, Brushes.Black, (float)(location.X - 20), (float)(location.Y));
        }
    }
    
    public class Renderer
    {
        private readonly PointF _center;
        private readonly float _radius;
    
        public Renderer(float centerX, float centerY, float radius)
        {
            _center = new PointF(centerX, centerY);
            _radius = radius;
        }
    
        public void Draw(Graphics g, Wheel wheel, Font font)
        {
            foreach (var bucket in wheel.Buckets)
            {
                var bucketLocation = new PointF(
                    (float)(_center.X + Math.Cos(bucket.Angle / (180.0 / Math.PI)) * _radius),
                    (float)(_center.Y + Math.Sin(bucket.Angle / (180.0 / Math.PI)) * _radius)
                );
                g.DrawLine(Pens.Black, _center, bucketLocation);
                bucket.Draw(g, bucketLocation, font);
            }
        }
    }

    Det går garanterat att justera acceleration, gravitation, inflödeshastighet, utflödeshastighet och maximal hastighet för andra (bättre?) resultat.

  • Poker formation checker for .NET 5.0

    Poker formation checker for .NET 5.0

    I got a mail from an unknown programmer who asked me to port my poker formation checker from .NET Framework to .NET 5.0. Here is the repository and here is the NuGet package. Happy coding!

  • C# 9 Records

    C# 9 Records

    Hur projektmallarna pytsas ut i Visual Studio har verkligen övergått mitt förstånd. Beträffande .NET 5.0 var Windows Forms-mallen tillgänglig långt innan Console Application-mallen, men nu är äntligen allt på plats. Med anledning av det vill jag summera nyheterna i C# version 9 som, för att fungera fullt ut, kräver .NET 5.0. Här är den första:

    Records
    Records är klasser där man gjort avkall på vissa möjligheter för att komma åt möjligheten skapa objekt med properties på så lite kod som möjligt. Nyckelordet record anger att de medlemmar som följer ska vara publika (vilket inte stämmer enligt nuvarande officiell version) och om de inte ska kunna modifieras i programkoden, kan de nu deklareras som init (vilket faktiskt fungerar), vilket betyder public fram till första tilldelningen, därefter private.

    using System;
    
    namespace ConsoleApp1
    {
        class Program
        {
            static void Main(string[] args)
            {
                var a = new Something {X = 10, Y = 20};
                Console.WriteLine(a.X);
            }
        }
    
        public record Something
        {
            public int X { get; init; }
            public int Y { get; init; }
        }
    }

    Skulle programmet fortsätta med tilldelningen a.X = 30 skulle kompilatorn påpeka att vår enda chans att lagra et värde i a.X är förbrukad.

  • Slow down (Commodore 64)

    Slow down (Commodore 64)

    En inte helt obetydlig del av min musiksmak kommer från Commodore 64, vars analoga musikkapacitet saknade motstycke. Vissa demos som släpptes var innehöll korta samplingar från kontemporär populärmusik, som t.ex. Slow down. När det handlar om digital musik var C64:an ytterst undermålig – minnet var för litet, antalet bitar per samplingspunkt var får lågt. Men digitala musikdemos släpptes, för att det gick. Ett av dessa var Slow down (länkad ovan) som jag inte hade den blekaste aning om varifrån den kom förrän YouTube dök upp. Nu vet jag att det handlade om ett snävt urval från maxiversionen av gruppen Loose ends singel med samma namn.

  • Hur svårt är det att gissa ett tal?

    Hur svårt är det att gissa ett tal?

    En person tänker på ett tal mellan 1 och 100, och en annan ska gissa vilket. Om den som gissar skulle gissa fel, så får han reda på om han har gissat på ett för stort eller för litet tal. Den bästa strategin för att gissa rätt på så få försök som möjligt, är att försöka diskvalificera så många tal som möjligt.

    Genom att gissa på 10 och få höra att talet är för stort, är genast 91 tal diskvalificerade, och rätt svar måste vara mellan 1 och 9, men man kan lika gärna (och troligen) få höra att talet är för litet, och då är bara 10 tal (1 till 10) diskvalificerade medan 90 tal fortfarande kan vara aktuella.

    Smartast är att gissa på 50. Är det fel svar, diskvalificerar det ändå fler än hälften av alla tal mellan 1 och 100, oberoende av om 50 är för stort eller inte. Skulle det vara för stort gissar man på 25, skulle det vara för litet gissar man på 75, och så vidare. Den vinnande strategin för den som gissar på ett tal är alltså att dra av hälften av talrymdens storlek och addera det till eller dra bort det ifrån senaste gissning.

    Eftersom det finns en bästa strategi för den som gissar, så kommer antalet försök att hitta ett tal alltid bli detsamma för ett specifikt tal. Personen tänker på talet 50, kommer den som gissar att hitta talet på första försöket. För att mäta hur lång tid det tar att hitta ett specifikt tal, och för att slippa sitta med en vän och gissa på tal hela veckan, har jag skapat två enkla robotar.

    Roboten Conny kan tänka på ett tal och be någon att gissa. Roboten Steven kan gissa på tal, och är dessutom bekant med gissningsstrategin jag beskrev ovan. Detta är Conny:

    public class NumberThinkingRobot
    {
        private readonly ICanGuess _guesser;
        private readonly int _correct;
    
        public NumberThinkingRobot(int correct, ICanGuess guesser)
        {
            _correct = correct;
            _guesser = guesser;
        }
    
        public void Begin()
        {
            var tryCount = 0;
            do
            {
                Console.WriteLine($"Try no. {++tryCount}");
    
                var guess = _guesser.Guess();
    
                if (guess < _correct)
                    _guesser.TooSmall();
                else if (guess > _correct)
                    _guesser.TooLarge();
                else
                {
                    Console.WriteLine("Correct!");
                    break;
                }
            } while (true);
        }
    }

    Och detta är Steven:

    public class NumberGuessingRobot : ICanGuess
    {
        private readonly int _max;
        private int _correct;
        private double _stepSize;
    
        public NumberGuessingRobot(int max)
        {
            _max = max;
            _correct = _max/2;
            _stepSize = _correct;
        }
    
        public int Guess()
        {
            Console.WriteLine($"Guess: {_correct}");
            return _correct;
        }
    
        public void TooSmall()
        {
            Console.WriteLine("Too small!");
    
            _stepSize /= 2.0;
            _stepSize = _stepSize < 1
                ? 1
                : _stepSize;
    
            _correct += (int)Math.Round(_stepSize);
            _correct = _correct > _max
                ? _max
                : _correct;
        }
    
        public void TooLarge()
        {
            Console.WriteLine("Too large!");
    
            _stepSize /= 2.0;
            _stepSize = _stepSize < 1
                ? 1
                : _stepSize;
    
            _correct -= (int)Math.Round(_stepSize);
            _correct = _correct < 1
                ? 1
                : _correct;
        }
    }

    Programmet presenteras i sin helhet sist. Så låt oss instruera Conny att det korrekta talet är 22, och instruera Steven att det högsta tillåtna talet är 100, för att sedan fösa ihop dem.

    using System;
    
    const int max = 100;
    const int correct = 22;
    var steven = new NumberGuessingRobot(max);
    var conny = new NumberThinkingRobot(correct, steven);
    conny.Begin();

    Vi kan konstatera att Steven behöver ha fem försök för att hitta 22. Detta är resultatet av körningen:

    Try no. 1
    Guess: 50
    Too large!
    Try no. 2
    Guess: 25
    Too large!
    Try no. 3
    Guess: 13
    Too small!
    Try no. 4
    Guess: 19
    Too small!
    Try no. 5
    Guess: 22
    Correct!

    Så hur svåra är de olika talen mellan 1 och 100 att hitta? 50 hittas direkt och 25 och 75 hittas på två gissningar. 13, 37, 63 och 87 kräver tre gissningar. 7 19, 31, 43, 57, 69, 81 och 93 kräver fyra. För 4, 10, 16, 22, 28, 34, 40, 46, 54, 60, 66, 72, 78, 84, 90 och 96 krävs fem gissningar. Resterande sextionio tal kräver sex gissningar eller fler, vilket innebär att det i snitt tar nästan sex gissningar att hitta rätt med den bästa strategin.

    Om man istället ska gissa på ett tal mellan 1 och 1000 så har vi tio gånger så många alternativ till korrekt tal, men strategin kräver bara i snitt nio gissningar. Om vi har hundra gånger så många alternativ (ett tal mellan 1 och 10.000) krävs i snitt ungefär 12 gissningar, och för att gissa ett tal mellan 1 och 100.000 krävs knappt 16 gissningar.

    Här är programmet i sin helhet (C#9):

    using System;
    
    const int max = 100;
    const int correct = 22;
    var steven = new NumberGuessingRobot(max);
    var conny = new NumberThinkingRobot(correct, steven);
    conny.Begin();
    
    public class NumberThinkingRobot
    {
        private readonly ICanGuess _guesser;
        private readonly int _correct;
    
        public NumberThinkingRobot(int correct, ICanGuess guesser)
        {
            _correct = correct;
            _guesser = guesser;
        }
    
        public void Begin()
        {
            var tryCount = 0;
            do
            {
                Console.WriteLine($"Try no. {++tryCount}");
    
                var guess = _guesser.Guess();
    
                if (guess < _correct)
                    _guesser.TooSmall();
                else if (guess > _correct)
                    _guesser.TooLarge();
                else
                {
                    Console.WriteLine("Correct!");
                    break;
                }
            } while (true);
        }
    }
    
    public class NumberGuessingRobot : ICanGuess
    {
        private readonly int _max;
        private int _correct;
        private double _stepSize;
    
        public NumberGuessingRobot(int max)
        {
            _max = max;
            _correct = _max/2;
            _stepSize = _correct;
        }
    
        public int Guess()
        {
            Console.WriteLine($"Guess: {_correct}");
            return _correct;
        }
    
        public void TooSmall()
        {
            Console.WriteLine("Too small!");
    
            _stepSize /= 2.0;
            _stepSize = _stepSize < 1
                ? 1
                : _stepSize;
    
            _correct += (int)Math.Round(_stepSize);
            _correct = _correct > _max
                ? _max
                : _correct;
        }
    
        public void TooLarge()
        {
            Console.WriteLine("Too large!");
    
            _stepSize /= 2.0;
            _stepSize = _stepSize < 1
                ? 1
                : _stepSize;
    
            _correct -= (int)Math.Round(_stepSize);
            _correct = _correct < 1
                ? 1
                : _correct;
        }
    }
    
    public interface ICanGuess
    {
        int Guess();
        void TooSmall();
        void TooLarge();
    }