Skip to main content

Christmas

In this project, you will create a Christmas decoration. The screen displays an image of Christmas and the speaker plays a Christmas song. Meanwhile, small snowflakes fall.

Circuit

Project overview

Example code

Please copy the image file into the SD card in advance.

The code used to play music is from here.

// Play the christmas song and display snow falling effect on screen.

import SwiftIO
import MadBoard
// The driver for the screen.
import ST7789
// The driver for the acceleromete.
import LIS3DH
// Graphical library.
import MadGraphics

@main
public struct Christmas {
public static func main() {
// Initialize the SPI pin and the digital pins for the LCD.
let spi = SPI(Id.SPI0, speed: 30_000_000)
let cs = DigitalOut(Id.D9)
let dc = DigitalOut(Id.D10)
let rst = DigitalOut(Id.D14)
let bl = DigitalOut(Id.D2)

// Initialize the LCD using the pins above. Rotate the screen to keep the original at the upper left.
let screen = ST7789(spi: spi, cs: cs, dc: dc, rst: rst, bl: bl, rotation: .angle90)

// Initialize the accelerometer using I2C.
let i2c = I2C(Id.I2C0)
let accelerometer = LIS3DH(i2c)

// A root tile is necessary to store all subtiles.
let rootTile = Tile<UInt16>(width: screen.width, height: screen.height, primaryColor: 0, isRoot: true)

// Get background image.
let imageBuffer = readImage(from: "/SD:/Christmas.bin")
let backgroundTile = Tile(width: screen.width, height: screen.height, bind: imageBuffer)
rootTile.append(backgroundTile)

// The count of snowflakes.
let snowCount = 120
// The size of snowflakes.
let snowSize = 11

// Store the snowflake tiles to update their position later.
var snowTiles: [Tile<UInt16>] = []

// The position range for snowflakes.
let positionRange = Array(0..<(screen.width-snowSize))

// Initialize snowflakes.
for _ in 0..<snowCount {
let snowTile = createSnowTile(at: getRandomPos())
snowTiles.append(snowTile)
rootTile.append(snowTile)
}

// Buffer used to store bitmap info.
var screenBuffer = [UInt16](repeating: 0, count: screen.width * screen.height)
var lineBuffer = [UInt16](repeating: 0, count: screen.width * screen.height)

// Store the bottom height to bank up snowflakes.
var bottomY = [Int](repeating: screen.height - 1 - snowSize, count: screen.width)

// Initialize the speaker.
let speaker = I2SOut(Id.I2SOut0, rate: 16_000, channel: .stereo)
// Initialize a player to play the music using generated audio data.
let player = Player(speaker, sampleRate: 16_000)

// Set the music player according to the music score.
player.bpm = MerryChristmas.bpm
player.timeSignature = MerryChristmas.timeSignature

// player.bpm = JingleBells.bpm
// player.timeSignature = JingleBells.timeSignature

// Store the current note in the track.
var noteIndex = 0

while true {
updateDisplay()

// The snowflakes fall consistently.
snow()

// // The snowflakes fall in accordance witht the accelerations.
// // Shake the board to restart the falling movement.
// snowUsingAccelerometer()

playMusic(
MerryChristmas.track,
waveform: MerryChristmas.trackWaveform,
amplitudeRatio: MerryChristmas.amplitudeRatio
)

// playMusic(
// JingleBells.track,
// waveform: JingleBells.trackWaveform,
// amplitudeRatio: JingleBells.amplitudeRatio
// )
}

// Play music using its score.
func playMusic(
_ track: [Player.NoteInfo],
waveform: Waveform,
amplitudeRatio: Float
) {
player.playNote(track[noteIndex], waveform: waveform, amplitudeRatio: amplitudeRatio)

noteIndex += 1
if noteIndex == track.count {
noteIndex = 0
}
}

// Generate starting position for snowflakes.
func getRandomPos() -> Point {
let x = positionRange.shuffled()[Int.random(in: positionRange.indices)]
let y = positionRange.shuffled()[Int.random(in: positionRange.indices)]
return (x, y)
}

// The snowflakes fall at a random speed to look more natural.
// After reaching the bottom, they will be repositioned and fall again.
func snow() {
for snowTile in snowTiles {
// Make the flake move down a bit each time.
// The horizontal movement simulates the influence of air movements.
var x = snowTile.x + Int.random(in: -1...1)
var y = snowTile.y + Int.random(in: 1...3)

// Kepp the flake within the screen.
if x < 0 {
x = 0
} else if x > screen.width - snowTile.width - 1 {
x = screen.width - snowTile.width - 1
}

// Update the flake's position after it's on the bottom.
if y > screen.height - snowTile.height - 1 {
let pos = getRandomPos()
x = pos.x
y = pos.y
}
snowTile.move(to: (x, y))
}
}

// The snowflakes fall and bank up on the bottom.
// The speed depends on the accelerations.
// You can shake the board back and forth to reposition the snowflakes and
// make them fall again.
func snowUsingAccelerometer() {
let values = accelerometer.readXYZ()

if values.y < -1 {
snowTiles.forEach { $0.move(to: getRandomPos()) }
for i in bottomY.indices {
bottomY[i] = screen.height - 1 - snowSize
}
} else {
for snowTile in snowTiles {
if snowTile.y < bottomY[snowTile.x] {
let dx = Int(Float(Int.random(in: 1...3)) * -values.x)
let dy = Int(Float(Int.random(in: 1...3)) * values.y)

var x = snowTile.x + dx
var y = snowTile.y + dy

if x < 0 {
x = 0
} else if x > screen.width - snowTile.width - 1 {
x = screen.width - snowTile.width - 1
}

if y < 0 {
y = 0
} else if y >= bottomY[snowTile.x] {
y = bottomY[snowTile.x]
for i in x..<x+snowTile.width {
bottomY[i] = y - snowTile.height
}
}
snowTile.move(to: (x, y))
}
}
}
}

// Draw snowflakes.
func createSnowTile(at point: Point) -> Tile<UInt16> {
let bitmap = Bitmap<UInt16>(width: snowSize, height: snowSize)
let color = Color.getRGB565LE(Color.white)
for x in 0..<snowSize {
bitmap.setPixel(at: (x, snowSize/2), color)
}
for y in 0..<snowSize {
bitmap.setPixel(at: (snowSize/2, y), color)
bitmap.setPixel(at: (y, y), color)
bitmap.setPixel(at: (y, snowSize - 1 - y), color)
}

return Tile<UInt16>(at: point, bitmap: bitmap, chromaKey: 0)
}

// Update the display after snowflakes change their position.
func updateDisplay() {
var dirtyRegions: [Region] = []
// Get the area that has been changed on the rootTile.
rootTile.getRefreshRegions(into: &dirtyRegions)

for dirtyRegion in dirtyRegions {
// Update the buffer with the new pixel info.
rootTile.update(region: dirtyRegion, into: &screenBuffer)

// Get the necessary pixel data from the screenBuffer which stores
// data for the entire tile.
var count = 0
for y in dirtyRegion.y..<(dirtyRegion.y + dirtyRegion.height) {
for x in dirtyRegion.x..<(dirtyRegion.x + dirtyRegion.width) {
lineBuffer[count] = screenBuffer[y * screen.width + x]
count += 1
}
}

// Send the data to the screen using SPI to update the specified area.
lineBuffer.withUnsafeMutableBytes {
screen.writeBitmap(
x: dirtyRegion.x, y: dirtyRegion.y,
width: dirtyRegion.width, height: dirtyRegion.height,
data: UnsafeRawBufferPointer($0)
)
}
}
// Reset all status of rootTile for next change.
rootTile.finishRefresh()
}

// Read image from SD card.
func readImage(from path: String) -> UnsafeBufferPointer<UInt16> {
let widthMask = UInt32(0x7FF) << 10
let heightMask = UInt32(0x7FF) << 21

var header = UInt32(0)
let headerSize = 4

let file = FileDescriptor.open(path)
defer { file.close() }

withUnsafeMutableBytes(of: &header) {
_ = file.read(into: $0, count: headerSize)
}

let width = (header & widthMask) >> 10
let height = (header & heightMask) >> 21
let bufferCount = Int(width * height)

let buffer = UnsafeMutableBufferPointer<UInt16>.allocate(capacity: bufferCount)
buffer.initialize(repeating: 0x0000)

let rawBuffer = UnsafeMutableRawBufferPointer(buffer)

_ = file.read(fromAbsoluteOffest: headerSize, into: rawBuffer, count: bufferCount * 2)

return UnsafeBufferPointer(buffer)
}
}
}